图片抓取思路

 qiu 

  当你的工作成为一种体力劳动,而自己无法从中获得新的体验和知识的时候,需要警惕究竟发生了什么。在一段安逸的日子后,尤其的需要做些尝试。几个月前的一次尝试,过程中由于一些原因没能进行下去,下面是我的一些尝试,总结一下。

做什么?

  抓取一个旅游景点网站的导览图片,并按照省市区+景区名称命名,以文件目录方式或mongodb存储。网站原貌左侧面板是城市区划目录树,右侧面板是对应行政区下的景区(分析发现最多显示该行政区下的100个景区);点击右侧面板景区如颐和园,重新又开启了一个页面,我们需要获取的目标图片为该页面下的景区游览图:
  meet99-guide   

设计思路

设计大体分为三个步骤;前两步为预处理前的准备,最后是下载;说来简单,中间有很多具体实现的细节要去考虑:

  1. 获取行政区划及其对应的网址;这部分因为几乎是固定的,抓取完毕即可按日期持久化存储为本地文件;–(预处理)
  2. 根据行政区划网址进一步分析并获取其下所有的景区链接地址,抓取完毕即可按日期持久化存储为本地文件;–(预处理)
  3. 根据行政区划下的景区链接进一步分析导览图片所的DOM节点对应的uri,下载文件,存储在mongodb中;–(抓取)

用到的一些开源库:

开源库 类型 备注
Json.Net 高效的.Net JSON框架库 解析响应返回的json数据
HtmlAgilityPack 一个支持用XPath来解析HTML的类库 解析前端html元素和标签
ScrapySharp 从Url获取Html数据,提供CSS选择器的方式解析Html节点 ScrapySharp的Html解析是基于大名鼎鼎的HtmlAgilityPack来实现的

  

问题1-403 Forbidden

  很自然自然想到分析导览图的url,通过后台伪造请求头,来批量请求地址下载导览图。尤其是需要伪造Referer,因为网站往往通过Referer来限定只有本站的请求才可以访问到图片,音频等资源,从而防止资源被盗链。

什么是HTTP Referer?
  简言之,HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。比如从我主页上链接到一个朋友那里,他的服务器就能够从HTTP Referer中统计出每天有多少用户点击我主页上的链接访问他的网站。
Referer其实应该是英文单词Referrer,不过拼错的人太多了,所以编写标准的人也就将错就错了。

接着按照思路开始请求了图片地址,结果出现403 Forbidden;

解决方案

咨询了老一辈程序员的些建议,并Google参考了下面两篇文章:

  1. 解决图片引用由于对方防盗链处理加载失败的解决方法
  2. 利用 Nginx 实现静态资源的反向代理

用nginx做反向代理,在配置文件nginx.conf中,http→server→location 下设置proxy_pass来指定当前代理的图片网站资源父目录的相对地址,程序中通过解析图片链接来获取图片名称并动态拼接在location所指定的url后面;设置proxy_set_header referer来指定Referer;

server {
    listen       8011;
    server_name  localhost;

    location /image/ {
        proxy_pass 代理资源地址;
        proxy_set_header referer  referer头信息;
        }
    }

问题2-IP频繁请求被限制

  此次尝试当请求到50条左右,服务端返回500错误,试了让主线程等待10秒后再次请求结果还是500错误;浏览器也无法访问主站,大概会被限制一天时间无法访问;
  从网上找了一些代理IP,通过Http代理设置类WebProxy的 方法类来代理访问,但全是失效的,还有一个代理IP实际跳转到它的网站;搞得我有点懵,难道是我方法不对?求大神们指导

代理IP有什么用?

  • 突破自身IP访问限制,访问国外站点。如:教育网、169网等网络用户可以通过代理访问国外网站。
  • 访问一些单位或团体内部资源,如某大学FTP(前提是该代理地址在该资源的允许访问范围之内),使用教育网内地址段免费代理服务器,就可以用于对教育 网开放的各类FTP下载上传,以及各类资料查询共享等服务。
  • 突破中国电信的IP封锁:中国电信用户有很多网站是被限制访问的,这种限制是人为的,不同Serve对地址的封锁是不同的。所以不能访问时可以换一个国 外的代理服务器试试。
  • 提高访问速度:通常代理服务器都设置一个较大的硬盘缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度。
  • 隐藏真实IP:上网者也可以通过这种方法隐藏自己的IP,免受攻击。

最后一点是”隐藏真实IP”是我们所关心的,那就可以用代理IP做网络爬虫。

解决思路

  1. 有的网站提供付费的高匿IP接口,可以批量获取,来做IP代理池
  2. adsl动态ip拨号服务器,动态切换断开来获取IP,没有深入了解

最后

  meet99-LXCrawler

2008年5.12写的一篇作文

tg_zx

  仍是鲜花开放的时节,为什么我看见的却是凋零的花瓣,摧折的残枝?
  仍是鸟雀啼鸣的正午,为什么我听见的却是破碎的音符,凄惨的呜咽?

前言

   2008年或许是属于很多80,90后人的记忆节点,那会我正读高二,在中学的教室里和大多同学一样做着总是做不完的练习册,考不完的试,每次到了下午班主任的物理课上总是头昏脑涨,昏昏欲睡。考试成绩一直是班级里的”中流砥柱”,总是想这次在多做几套题下次应该可以考的好点,呵呵,但是直到高考结束也一直来没有给我带来什么surprise。反而是08年以纪念“汶川地震”为主题的一篇小作文,极其意外地获得了我们县当时高中组某个征文比赛的一等奖,后来在陕西省也拿了个奖。我当时压根不知道有这个比赛,在此之前我的语文成绩真的很差,因为不会读声调加上字写的不好,被老师没少点名要求课后抄写课文,因此也只是像往常一样写了篇作文完成任务交了上去。
   不知觉间,历史的滚轮,已从中学教室里走过8个春夏秋冬,还记得那个夏日的闷热的下午,奶奶早早地把饭做好了等我回去吃饭,爷爷喊我们兄弟三个过去把冰箱里的西瓜切了吃了,7点准时做在电视机前的沙发上看着第二天的天气预报,日历上记录着我们的生日和写满的了第二天的天气。那个时候,我从来没有想过有一天爷爷奶奶离开的时候,后来爷爷在2012年走了,奶奶也在去年11月追随而去。虽然他们离开了我们,可那些平凡的生活画面,成长岁月的点滴陪伴,却是心中永远的难忘,将会一直陪伴着我们一家人!
   重新翻起这篇作文,想起了爷爷奶奶,想起了写作文的那个下午,想起了那些远去的人,我想说这一天人们并没有忘记你们……


呼唤生命

  美少年海辛瑟斯是希腊神话中的植物神,他因被误伤而失去了生命,在他鲜血染红的土地上开出一朵美丽的花,这花就是风信子,它无时无刻都在给人们美的享受.
  花语:只要点燃生命之火,便可同享丰盛人生.
  二零零八年必将是一个充满悲伤和喜悦的一年.年初南方下大雪;五月十二日四川发生大地震;接着山东又发生铁路上火车相撞事件;八月八日又迎来期盼百年的奥运会的开幕;接下来又发生了三鹿奶粉和全球金融风暴等等一系列的重大事件.我想历史将铭记二零零八年所发生的着一切.
  现在时隔大地震已过去半年了,不知道灾区里的同胞是否还好,生活是否还和以前一样.我想我永远也忘不了那举国震惊的一刻.“5.12地震”后的几天我们从电视中看到了很多令人震惊的画面.那一座座的楼房倒塌,钢筋混凝土像被黑暗的恶魔撕裂似的完全裸露在外面,尤其还有和我们一样的学生,他们本该和我们一样此刻正在教室里读那一个个英语单词,等待着放学回家后妈妈的一桌早已准备好的饭菜,也怀着和我们一样对未来美好的憧憬,但现在我看到的却是一排排安静地躺在地面上的他们.他们中有的父母不停地嘶喊,父母们双手紧握着他们那渐已冰凉的手不停的哭泣,那哭声是那么的撕心裂肺;老师们此时却并没有去找他们的子女,而是一直在旁边为压在房屋下的学生鼓励,让他们一定要坚持下来.周围到处都是忙碌的人群,有的在打电话,医生护士在抢救着病人,人民军队正冒者余震的危险在赢救和找寻着每一个幸存的生命.那些学生却依然静静的躺在地面上.
  记得地震发生的前一天正是母亲节,我想我的同龄人们也都想着为母亲做点事,让她们好好歇息一天.自觉尝试炒几盘菜,送妈妈一束康乃馨,或者是考试考了一百分等等诸如此类的事来让妈妈高兴,觉得生活是这么的美好.可是,你看看着犹如魔鬼般的地震所做的一切.它使父母失去子女,子女也永远失去父母,到处都是废墟,到处都有人们的悲伤之声.
  仍是鲜花开放的时节,为什么我看见的却是凋零的花瓣,摧折的残枝?
  仍是鸟雀啼鸣的正午,为什么我听见的却是破碎的音符,凄惨的呜咽?
此刻我多么希望躺在地上的他们正在做着一场恐怖的梦,醒来后仍然是阳光明媚的中午,仍在听老师讲着课.但这一切却显得那么的真实.温总理说过:活着,就要好好地活下去,你们是民族的脊梁.是的,愿生命如花,愿风信子的花语永远传递:珍惜生命,同享生命.
.

一次简单的地图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   

  

大四那年,空间分析的一个demo

这两天参加了个学校举办的一个GIS制图和系统设计的比赛,用ArcEngine+c# 做了个演示用的“城市购房分析GIS系统” 。

系统需要实现的空间分析功能:

所寻求的买房区域要满足以下条件

(1) 离主要交通要道200米之外,以减少噪声(ST为道路数据中类型为交通要道的要素)。

  • 在商业中心的服务范围之内,服务范围以商业中心规模的大小(属性字段为YUZHI)来确定。
  • 距名牌高中750米之内,以便小孩上学便捷。
  • 距名胜古迹500米之内,环境幽雅。

(2) 对每个条件进行缓冲区分析,将符合条件的区域取值为1,不符合条件的取值为0,得到各自的分值图。 (需要实现增加删除编辑相关要素字段)

(3) 运用空间叠置分析对上述4个图层叠加求和,并分等定级,确定适合的区域。

设计的系统主界面:(是想用Metro风格的,扁平化UI设计看起来舒服)
 此处输入图片的描述

  针对要素图层的字段值进行空间查询,由选择要素生成缓冲区,也可以由图层生成(通过设定缓冲距离或字段进行动态缓冲区生成),调用的GP工具。其中根据字段动态生成缓冲区,在ArcMap里有问题,调用的GP工具实现也有问题。具体表现为在ArcMap里不响应,在自己做程序里程序死掉。问题不解?  

以下是根据各要素条件生成的缓冲区图:

  1. 道路要素(根据ST字段代表的主干道生成缓冲区)
    此处输入图片的描述
  2. 学校要素缓冲区(750m)
    此处输入图片的描述
  3. 名胜古迹缓冲区(500m)
    此处输入图片的描述
  4. 商业中心缓冲区(那个根据字段动态生成始终有问题,随后改为500m)
    此处输入图片的描述
  5. 商业中心,学校,名胜古迹三者缓冲区求交
    此处输入图片的描述
  6. 用道路来擦除交集要素缓冲区
    此处输入图片的描述

  另外再做了一些其他东西,如字段计算器之类的,不过主要还是实现空间分析方面。都是基于属性表的求交,擦除,缓冲分析,而不光是几何图像的位置关系。