前兩篇文章分析了我為什麼抓取摩拜單車的http://www.php.cn/code/11829.html" target="_blank">介面以及資料分析的結果,這篇文章中講直接提供可啟動並執行原始碼供學習。
聲明:
此爬蟲僅用於學習、研究用途,請不要用於非法用途。任何由此引發的法律糾紛自行負責。
沒耐心看文章的請後直接:
git clone https://github.com/derekhe/mobike-crawlerpython3 crawler.py
爽了以後請別忘了給個star和!
目錄結構
\analysis - jupyter做資料分析
\influx-importer - 匯入到influxdb,但之前沒怎麼弄好
\modules - 代理模組
\web - 即時圖形化顯示模組,當時只是為了學一下react而已,效果請見這裡
crawler.py - 爬蟲核心代碼
importToDb.py - 匯入到postgres資料庫中進行分析
sql.sql - 建立表的sql
start.sh - 持續啟動並執行指令碼
思路
核心代碼放在crawler.py中,資料首先儲存在sqlite3資料庫中,然後去重複後匯出到csv檔案中以節約空間。
摩拜單車的API返回的是一個正方形地區中的單車,我只要按照一塊一塊的地區移動就能抓取到整個大地區的資料。
left,top,right,bottom定義了抓取的範圍,目前是成都市繞城高速之內以及南至南湖的正方形地區。offset定義了抓取的間隔,現在以0.002為基準,在DigitalOcean 5$的伺服器上能夠15分鐘內抓取一次。
def start(self): left = 30.7828453209 top = 103.9213455517 right = 30.4781772402 bottom = 104.2178123382 offset = 0.002 if os.path.isfile(self.db_name): os.remove(self.db_name) try: with sqlite3.connect(self.db_name) as c: c.execute('''CREATE TABLE mobike (Time DATETIME, bikeIds VARCHAR(12), bikeType TINYINT,distId INTEGER,distNum TINYINT, type TINYINT, x DOUBLE, y DOUBLE)''') except Exception as ex: pass
然後就啟動了250個線程,至於你要問我為什麼沒有用協程,哼哼~~我當時沒學~~~其實是可以的,說不定效率更高。
由於抓取後需要對資料進行去重,以便消除小正方形地區之間重複的部分,最後的group_data正是做這個事情。
executor = ThreadPoolExecutor(max_workers=250) print("Start") self.total = 0 lat_range = np.arange(left, right, -offset) for lat in lat_range: lon_range = np.arange(top, bottom, offset) for lon in lon_range: self.total += 1 executor.submit(self.get_nearby_bikes, (lat, lon)) executor.shutdown() self.group_data()
最核心的API代碼在這裡。小程式的API介面,搞幾個變數就可以了,十分簡單。
def get_nearby_bikes(self, args): try: url = "https://mwx.mobike.com/mobike-api/rent/nearbyBikesInfo.do" payload = "latitude=%s&longitude=%s&errMsg=getMapCenterLocation" % (args[0], args[1]) headers = { 'charset': "utf-8", 'platform': "4", "referer":"https://servicewechat.com/wx40f112341ae33edb/1/", 'content-type': "application/x-www-form-urlencoded", 'user-agent': "MicroMessenger/6.5.4.1000 NetType/WIFI Language/zh_CN", 'host': "mwx.mobike.com", 'connection': "Keep-Alive", 'accept-encoding': "gzip", 'cache-control': "no-cache" } self.request(headers, payload, args, url) except Exception as ex: print(ex)
最後你可能要問頻繁的抓取IP沒有被封嗎?其實摩拜單車是有IP的訪問速度限制的,只不過破解之道非常簡單,就是用大量的代理。
我是有一個代理池,每Apsara Infrastructure Management Framework本上有8000以上的代理。在ProxyProvider中直接擷取到這個代理池然後提供一個pick函數用於隨機選取得分前50的代理。請注意,我的代理池是每小時更新的,但是代碼中提供的jsonblob的代理列表僅僅是一個範例,過段時間後應該大部分都作廢了。
在這裡用到一個代理得分的機制。我並不是直接隨機播放代理,而是將代理按照得分高低進行排序。每一次成功的請求將加分,而出錯的請求將減分。這樣一會兒就能選出速度、品質最佳的代理。如果有需要還可以存下來下次繼續用。
class ProxyProvider: def init(self, min_proxies=200): self._bad_proxies = {} self._minProxies = min_proxies self.lock = threading.RLock() self.get_list() def get_list(self): logger.debug("Getting proxy list") r = requests.get("https://jsonblob.com/31bf2dc8-00e6-11e7-a0ba-e39b7fdbe78b", timeout=10) proxies = ujson.decode(r.text) logger.debug("Got %s proxies", len(proxies)) self._proxies = list(map(lambda p: Proxy(p), proxies)) def pick(self): with self.lock: self._proxies.sort(key = lambda p: p.score, reverse=True) proxy_len = len(self._proxies) max_range = 50 if proxy_len > 50 else proxy_len proxy = self._proxies[random.randrange(1, max_range)] proxy.used() return proxy
在實際使用中,通過proxyProvider.pick()選擇代理,然後使用。如果代理出現任何問題,則直接用proxy.fatal_error()降低評分,這樣後續就不會選擇到這個代理了。
def request(self, headers, payload, args, url): while True: proxy = self.proxyProvider.pick() try: response = requests.request( "POST", url, data=payload, headers=headers, proxies={"https": proxy.url}, timeout=5,verify=False ) with self.lock: with sqlite3.connect(self.db_name) as c: try: print(response.text) decoded = ujson.decode(response.text)['object'] self.done += 1 for x in decoded: c.execute("INSERT INTO mobike VALUES (%d,'%s',%d,%d,%s,%s,%f,%f)" % ( int(time.time()) * 1000, x['bikeIds'], int(x['biketype']), int(x['distId']), x['distNum'], x['type'], x['distX'], x['distY'])) timespend = datetime.datetime.now() - self.start_time percent = self.done / self.total total = timespend / percent print(args, self.done, percent * 100, self.done / timespend.total_seconds() * 60, total, total - timespend) except Exception as ex: print(ex) break except Exception as ex: proxy.fatal_error()
好了,基本上就到此了~~~其他的代碼自己研究吧~~~