背景:端午节假期的时候参加了学校的数学建模比赛,题目是关于共享单车的供需匹配问题,需要获得共享单车的数量和时空分布情况。
在苦苦找寻数据无果的情况下决定自己用爬虫对天津地区的mobike进行统计。
在网上找到了这篇爬虫的代码,本着少造轮子的基本原则,我选择了这个代码进行统计,这里记录一下历程,方便日后查阅。
先上原作者github地址:git clone https://github.com/derekhe/mobike-crawler
。python3环境,爬取的是微信小程序,之前是可以爬手机客户端的,不过随着客户端完善变得难爬,小程序的爬取
能用到什么时候要也不确定。
我会把我的一些代码和数据放到百度云里,链接在文末,有需要的小伙伴可以自取。
一、数据之初探
首先确定了要爬取微信小程序的时候,先需要确定一个大概的方向,下载fiddler,对微信小程序进行抓包处理,这一步进展比较顺利。不太了解这一步的可以搜索手机APP抓包相关的知识,这里不再赘述。
比较容易的发现在请求页面需要post你的经纬度坐标给https://mwx.mobike.com/mobike-api/rent/nearbyBikesInfo.do这个网页,然后返回周围的车的信息,包括
bikeID,distX,distY,distance等信息,比较方便的是这些信息都放在了json文件中。而我们关系的只是车的位置和距离即distX,distY,distance。
注意点是要把浏览器头部伪装成手机浏览器。
z.json()['object']即可。
import requests headers = { 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E304 MicroMessenger/6.5.7 NetType/WIFI Language/zh_CN', 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://servicewechat.com/wx80f809371ae33eda/23/page-frame.html', } url = 'https://mwx.mobike.com/mobike-api/rent/nearbyBikesInfo.do' data = { 'longitude':'121.1883',# 经度 'latitude':'31.05147', # 纬度 'citycode':'021', 'errMsg':'getMapCenterLocation:ok' } z = requests.post(url,data=data,headers=headers,verify=False)
二、数据之再思考
有了第一步的初探,下一步需要做的东西也十分清楚。要获得天津地区的Mobike数量可以近似画一个长方形,用爬虫爬取这个长方形内的自行车数目即可。
直接贴原作者的代码,我的代码在其中做了一些修改,不过我是放在了jupyter notebook中运行的,页面中有将近十万的运行结果数据的原因,每次打开代码都会卡死。。。
需要注意的点是,在我电脑上运行时找不到retrying库,直接去掉即可运行。运行结果放在db文件夹下。
offset设置为0.002左右即可,太大容易导致漏数,太小会有重复误差。这也是网上有其他代码用数据库的原因,不过由于数学建模的时间紧张,再加我对数据库不太熟悉,直接暴力设定爬取步长也没出现问题。
import datetime import os import os.path import random import sqlite3 import threading import time import ujson from concurrent.futures import ThreadPoolExecutor import numpy as np import requests from retrying import retry from modules.ProxyProvider import ProxyProvider class Crawler: def __init__(self): self.start_time = datetime.datetime.now() self.csv_path = "./db/" + datetime.datetime.now().strftime("%Y%m%d") os.makedirs(self.csv_path, exist_ok=True) self.csv_name = self.csv_path + "/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + '.csv' self.db_name = "./temp.db" self.lock = threading.Lock() self.proxyProvider = ProxyProvider() self.total = 0 self.done = 0 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) 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() 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 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() def group_data(self): print("Creating group data") conn = sqlite3.connect(self.db_name) cursor = conn.cursor() f = open(self.csv_name, "w") for row in cursor.execute('''SELECT * FROM mobike'''): timestamp, bikeIds, bikeType, distId, distNumber, type, lon, lat = row f.write("%s,%s,%s,%s,%s,%s,%s,%s " % ( datetime.datetime.fromtimestamp(int(timestamp) / 1000).isoformat(), bikeIds, bikeType, distId, distNumber, type, lon, lat)) f.flush() f.close() os.system("gzip -9 " + self.csv_name) Crawler().start()
三、数据简单处理和分析
爬取天津主城区(红桥区,河西区,河东区,和平区,南开区,河北区)和周边范围的Mobike,
爬取结果大概为98398辆,利用Kmeans聚类进行聚类分析。
分别取聚类点50,75,100,150,200来通过matplotlib绘图观察聚类结果。选择n_clusters=50。
得到聚类中心点坐标,然后再利用反解析软件解析坐标对应的地点信息,即可获得共享单车的主要分布区域。
最后,一些数据和代码放在百度云上,用则自取。
百度云地址:http://pan.baidu.com/s/1eRHjmVK密码:d5dw
以上。
:)
(2019.03.21更新:由于去年一整年都在备战考研,很多消息没来得及回。不幸的是我去年的时候那台老惠普死掉了,数据集也丢在了里边,所以目前我手上也没有数据集了,抱歉。)