一次简单的地图POI点数据抓取

为什么写博客?

《暗时间》的作者刘文鹏写过一篇著名的博文《为什么你应该从现在开始就写博客》,其中有几句话,让我更加坚信快速成长中的程序员,在当下互联网技术爆炸式发展的环境下应该去拥有一个学习积累,沉淀讨论的博客。摘录如下:

  • 书写是为了更好的思考
  • “教”是做好的”学”
  • 讨论是绝佳的反思
  • 激励持续的学习和思考
  • 学会持之以恒地做一件事情
  • 一个长期的价值博客是一份很好的简历

需求背景

  大概2个月前,一姑娘做毕业设计,课题是“城市银行网点的空间分析”,其中需要某市下所有的atm名称和其所在的位置坐标,以shpfile格式存储数据(包含坐标文件(.shp),索引(.shx),属性文件(.dbf))。一年前用python 抓过此类带有位置属性的面数据。这次打算用c#抓一下,相比于java,python,个人比较喜欢c#,从大学时就在用,熟悉的属性字段,熟悉的IDE,windows系统上不用额外的部署运行环境,可谓是拿来即用。

数据从那获得?

目标数据来源是国内Top2的两家地图之一,获取方式一般有以下两种
开放平台
  根据LBS开放平台提供的各种web服务API来请求数据,一般需要注册一个账号获取key。优点是完善的接口文档和调用比较方便;缺点是通常有请求次数限制,提供的服务接口从深度和广度上都比教少,并且返回的信息量也比较少。

地图网站
  查看跟踪地图网站的HTTP请求,分析查询参数并构造数据,后端使用Request工具类构造浏览器请求头(比如UserAgent和Referer防盗链),请求数据获取Response。优点是不会受到明确的请求次数限制,获取的数据信息从内容和量上都更丰富;缺点是需要去分析接口,以及频繁请求后IP受限等,各种与服务端反爬策略斗智斗勇喽。

从开放平台提供的接口上并没有atm点位置的数据,综合以上,果断选用从“地图网站”抓取数据。


怎么从地图网站获取数据?

  1. 首先很自然的想到如何从地图上看到这个城市的atm点分布情况?
      访问某地图的网址,切换到目标城市搜索框输入关键词: atm
  2. 在搜索框输入关键词时,我在想这些模糊匹配的数据从何而来?

    bd_key_pic

  用Chrome浏览器 F12 唤出控制台切换到Network里选中XHR(ajax请求)可以看到在搜索框输入关键词内容变化的瞬间,后台在进行ajax请求并响应输出模糊匹配的结果,这些数据被包裹在搜索框的下拉列表中。至于搜索匹配策略和执行效率,具体的服务端和客户端缓存细节不是很清楚,但和这次抓点数据无关,有时间在去了解下。

  1. 最后当我点击搜索时如预料之中看到了一个个气泡点展现在地图上(还好没有提示我滑动验证或IP被拒绝访问),那么进一步就想看看地图上这些气泡点数据从那来的?
      
    bd_atm_pic

  还是唤起控制台在Network里选中XHR(ajax请求),查一下搜索时调用的具体接口,Chrome浏览器装上JSONView扩展(格式化显示json数据),在新标签页打开这个链接。

4. 仔细分析一下不难发现 location这个字段,结合其他字段描述信息,地图上的atm气泡点数据坐标源来从此获得。ok分析完后,此时也大致有了编码的思路。
amap_atm_info


编码片段

  1. 准备阶段
    建议使用NuGet管理依赖的第三方程序包,VS2013已经集成了NuGet,用高版本的VS吧,那些老旧的IDE让它们见鬼去吧,以下是几个开源工具类的介绍。
开源库 类型 备注
Json.Net 高效的.Net JSON框架库 解析响应返回的json数据
DotSpatial 基于.Net 4.0的GIS库 本次用它生成shp数据
Winista.HtmlParser .Net平台解析Html的词法分析器 做了个异步请求的demo
  1. 部分代码
    一个通用的c#请求工具类(GET和POST):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    class HttpWebResponseUtility
    {
    private static readonly string DefaultUserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";

    /// <summary>
    /// 创建GET方式的HTTP请求
    /// </summary>
    /// <param name="url">请求的URL</param>
    /// <param name="timeout">请求的超时时间</param>
    /// <param name="userAgent">请求的客户端浏览器信息,可以为空</param>
    /// <param name="cookies">随同HTTP请求发送的Cookie信息,如果不需要身份验证可以为空</param>
    /// <returns></returns>
    public static HttpWebResponse CreateGetHttpResponse(string url, int? timeout, string userAgent, CookieCollection cookies)
    {
    if (string.IsNullOrEmpty(url))
    {
    throw new ArgumentNullException("url");
    }
    HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
    request.Method = "GET";
    request.UserAgent = DefaultUserAgent;
    if (!string.IsNullOrEmpty(userAgent))
    {
    request.UserAgent = userAgent;
    }
    if (timeout.HasValue)
    {
    request.Timeout = timeout.Value;
    }
    if (cookies != null)
    {
    request.CookieContainer = new CookieContainer();
    request.CookieContainer.Add(cookies);
    }
    return request.GetResponse() as HttpWebResponse;
    }

    /// <summary>
    /// 创建POST方式的HTTP请求
    /// </summary>
    /// <param name="url">请求的URL</param>
    /// <param name="parameters">随同请求POST的参数名称及参数值字典</param>
    /// <param name="timeout">请求的超时时间</param>
    /// <param name="userAgent">请求的客户端浏览器信息,可以为空</param>
    /// <param name="requestEncoding">发送HTTP请求时所用的编码</param>
    /// <param name="cookies">随同HTTP请求发送的Cookie信息,如果不需要身份验证可以为空</param>
    /// <returns></returns>
    public static HttpWebResponse CreatePostHttpResponse(string url, IDictionary<string, string> parameters, int? timeout, string userAgent, Encoding requestEncoding, CookieCollection cookies)
    {
    if (string.IsNullOrEmpty(url))
    {
    throw new ArgumentNullException("url");
    }
    if (requestEncoding == null)
    {
    throw new ArgumentNullException("requestEncoding");
    }
    HttpWebRequest request = null;
    //如果是发送HTTPS请求
    if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
    {
    ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult);
    request = WebRequest.Create(url) as HttpWebRequest;
    request.ProtocolVersion = HttpVersion.Version10;
    }
    else
    {
    request = WebRequest.Create(url) as HttpWebRequest;
    }
    request.Method = "POST";
    request.ContentType = "application/x-www-form-urlencoded";

    if (!string.IsNullOrEmpty(userAgent))
    {
    request.UserAgent = userAgent;
    }
    else
    {
    request.UserAgent = DefaultUserAgent;
    }

    if (timeout.HasValue)
    {
    request.Timeout = timeout.Value;
    }
    if (cookies != null)
    {
    request.CookieContainer = new CookieContainer();
    request.CookieContainer.Add(cookies);
    }
    //如果需要POST数据
    if (!(parameters == null || parameters.Count == 0))
    {
    StringBuilder buffer = new StringBuilder();
    int i = 0;
    foreach (string key in parameters.Keys)
    {
    if (i > 0)
    {
    buffer.AppendFormat("&{0}={1}", key, parameters[key]);
    }
    else
    {
    buffer.AppendFormat("{0}={1}", key, parameters[key]);
    }
    i++;
    }
    byte[] data = requestEncoding.GetBytes(buffer.ToString());
    using (Stream stream = request.GetRequestStream())
    {
    stream.Write(data, 0, data.Length);
    }
    }
    return request.GetResponse() as HttpWebResponse;
    }
    }

    /// <summary>
    /// 执行请求
    /// </summary>
    /// <param name="response">响应信息</param>
    /// <param name="url">爬虫的url</param>
    /// <param name="timeout">超时</param>
    /// <returns>请求结果</returns>
    public static AnalyResult ExcuteRequest(ref HttpWebResponse response,string url, int timeout)
    {
    AnalyResult analyStatus = null;
    try
    {
    response = HttpWebResponseUtility.CreateGetHttpResponse(url, timeout, null, null);
    if (response.StatusCode != HttpStatusCode.OK)
    analyStatus = new AnalyResult(AnalyResult.FailCode, "Http响应值异常:" + response.StatusCode);
    else
    analyStatus = new AnalyResult(AnalyResult.SuccessCode, "OK");
    }
    catch (HttpException ex)
    {
    // 发生致命的异常,可能是协议不对或者返回的内容有问题
    //Console.WriteLine("HttpException,原因:" + ex.Message);
    analyStatus = new AnalyResult(AnalyResult.RequestErrorCode, "HttpException:" + ex.Message);
    }
    catch (IOException ex)
    {
    // 发生网络异常
    //Console.WriteLine("IOException,原因:" + ex.Message);
    analyStatus = new AnalyResult(AnalyResult.RequestErrorCode, "IOException:" + ex.Message);
    }
    catch (Exception ex)
    {
    //Console.WriteLine("Exception,原因:" + ex.Message);
    analyStatus = new AnalyResult(AnalyResult.RequestErrorCode, "Exception:" + ex.Message);
    }
    return analyStatus;
    }

封装一个响应类来包裹返回的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class AnalyResult
{
public static readonly int SuccessCode = 200;
public static readonly int FailCode = 100;
public static readonly int RequestErrorCode = 300;
/// <summary>
/// 100:失败 200:成功
/// </summary>
public int Code { set; get; }
/// <summary>
/// 失败或成功返回信息
/// </summary>
public string Info { set; get; }

public AnalyResult(int code,string info)
{
this.Code = code;
this.Info = info;
}
}

问题和尝试

  • 做了个WinForm程序,用ListView控件动循环请求接口并动态填充数据行,遇到了数据量狂刷之下,UI线程在填充界面时会导致程序出现卡死未响应状态,网上有一个重写ListView控件的代码,稍微重写了向控件通知消息的部分,效果只能说是凑合用,后续有时间优化下这部分。

  • 在请求接口数据时,用Winista.HtmlParser分析了网页节点结构(这部分只是实验用,对于本次抓取没啥用),主要是在UI线程这种异步请求完成后在回调函数中获取请求的结果,并通过在前期捕捉到的UI调用线程的上下文来Post调用委托方法和传入参数,最后相当于UI线程递归填充了自己的TreeView,效果比较好

    谨记: WinForm 程序数据量稍大的操作千万别让UI线程来做,大数据量异步回调,让框架本身的线程池帮你去调度细节,看看这篇文章APM异步编程模型


写在最后

  记得大一大二时,有段时间很苦恼为啥我们写的程序都是在黑框框里运行,全是什么打印五角星图案此类的,没有一点成就感。有一天在图书馆找了一本c++ MFC的书,第一次看到原来这个可以写出所谓的界面”软件”出来,欣喜的把那本书的代码在VC6.0上敲了一遍,呵呵,最后好像是加了几个按钮,绘出了一个图形来。后来看到c# winform拖拉控件更方便更快,也自然地喜欢上了c#了,尝试过Devxpress的,IrisSkin等皮肤控件,Ribbion,Metro 风格的设计。哈哈,我果然是一个美工程序员。
  如今web和移动是当今的主流,现在也主要在用java做后端服务,以后应该很少在会去做桌面端的WinForm编程了。不过想起在学生阶段是WinForm程序让我对软件设计有了点滴的成就感,并喜欢上了编程,人生路上,想起自己的初心,想想当年的自己,在路上,未走丢。
  最后,结果图来一张,后边有时间代码放Github里。
   atm