1、项目背景

1.1 使用谷歌街景图片的必要性

  • MIT Place Pulse 数据集可直接下载,但没有提供街景图片本身,只提供了街景的坐标,需通过谷歌街景开放API 获取对应的街景图片。
  • MIT Place Pulse数据集中的街景图片大多在国外,因此你懂得。

1.2 使用谷歌街景图片的目标

  • “建立街景图片与人主观感受的联系”场景的相关论文都没有提供开源代码,需实现模型并训练,所以需要 MIT Place Pulse数据集作为基础。

1.3 “建立街景图片与人主观感受的联系”场景实现的基本流程:

  • 通过 MIT Place Pulse数据集以及相关街景图片训练模型。
  • 获取百度地图街景图片作为模型输入,通过上一步训练好的模型,获取结果(例如,对街景的治安状况进行评分等)。

1.4 参考链接

这篇文章详述用Python爬取该训练集,提供了训练集地址,此外还提供了多个可用的google street view static api key。链接如下:https://zhuanlan.zhihu.com/p/34967038
       下载好文中所述的训练集文件之后,仔细查看votes.csv及readme.txt文件。写的很清楚,需要对应votes.csv中的每一条数据,拼接街景图片下载的url。vote.csv文件内容如下,一行记录中有两个坐标,通过进一步观察发现,里面也有重复的坐标(街景ID)。因此我们在真正下载图片或拼接url之前还需做一次去重。

       当然在这之前还需要申请 google 云控制台的street view static api key,我们也可以直接采用上述文中api.txt文件中的key,但其中大多已不能使用。毕竟是公开的资源,大家都在用,很容易被限制,最好自己和团队成员多申请几个,申请时需要用到VISA信用卡。申请链接如下:https://developers.google.com/maps/documentation/streetview/get-api-key
       在有可用key的情况下,我们就可以通过发送GET请求的方式获取街景图片,对应的url如下:

https://maps.googleapis.com/maps/api/streetview?size=400x300&location=39.737314,-104.87407400000001&key=YOUR_API_KEY

2、任务分解

业务逻辑流程梳理大致如下:

  1. 解析vote.csv文件,并遍历每一条记录;
  2. 根据解析出的每一个坐标,判断该记录对应的图片是否已下载;
  3. 若已下载,则略过;
  4. 若未下载,则拼接url;
  5. 发送GET请求下载图片,因为是IO密集型任务,开启线程池进行并发下载;
  6. 存储(项目需求是存储至本地文件夹下即可)

2.1 csv文件的解析

可用于csv文件解析的工具有很多,如:javacsv、Inputstream等,强烈建议使用现成的优秀工具,不建议自己编写解析逻辑。更不建议一次性读入文件再进行解析。这里采用了一个号称是目前为止最高效的解析工具:univocity-parser,可采用迭代(行扫描)的方式读取每一条记录,详见:https://github.com/uniVocity/univocity-parsers。
       univocity-parser使用方法参考:https://blog.csdn.net/qq_21101587/article/details/79803582, 这里不再赘述。

2.2 街景ID(坐标)去重

这里需要注意的是,vote.csv文件有将近123万行数据记录,也就是近246万个坐标(含重复),如果一次性读入文件,并存入HashSet的话可能会引起OOM,如果该文件有上亿条数据记录,此方法更不可取。笔者采用的是redis去重,结合redis近乎O(1)的复杂度,能够处理数据量较大情况下的去重。但本训练集数量还远没有达到海量级别,用File类中的exists方法也可以去重。

2.2.1 使用redis去重:

关于街景坐标去重逻辑主要运用了以下几个命令:

//当redis中存在该key时,跳过;不含该key时,则存入该键值数据
jedis.setnx(key,value);//检查该key是否存在
jedis.exists(key);//获取以spider-hgg-googlemap:为正则前缀的所有key集合,返回set
jedis.keys("spider-hgg-googlemap:*");//删除该key
jedis.del(key);
2.2.2 使用File类的exists()去重

笔者原本以为new File(“文件路径”).exists()方法会随着本地文件中的图片越来越多而查询变慢,但在实际使用过程中发现该方法在本地图片达到6万多张的时候,执行时间也是毫、微秒级,因此也能高效完成去重。底层原理可能得益于文件索引也是用的B树或哈希索引的方式(本人自己猜测的,没有深入研究)
       去重代码就很简单了,传入参数ID,拼接图片路径即可:

private boolean isPicExists(String panoId){String path = "E:\\temp\\hgg-googlemap\\safety\\"+panoId+".jpg";File file = new File(path);return file.exists();
}

2.3 url的拼接

这里要注意一点的是,一个key每天的请求上限是2万次(本人亲测是低于2万次/天,不稳定),超过之后就会被限制访问,所以尽量获取更多的key,在拼接url的时候也尽量在有效的key集合中随机选择使用(为确保快速并可靠的下载,及时剔除无效的key),尽可能减少同一key频繁访问的次数。另外一点需要注意的是,需加一个判断图片是否下载成功的逻辑,若下载成功就存储,若不成功还要重新拼接url进行再次下载,直至成功为止。

2.4 线程池的使用

这一块涉及线程池的使用及线程数合理配置,不熟悉的童鞋可参阅:https://www.cnblogs.com/dolphin0520/p/3932921.html

一般需要根据任务的类型来配置线程池大小:

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 CPU核数量+1
  • 如果是IO密集型任务,参考值可以设置为2* CPU核数量
  • 当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

3 代码实现

3.1 添加依赖

dependencies {compile 'com.squareup.okhttp3:okhttp:3.11.0'compile 'com.demo.ddc:ddc-core:0.1.11-alpha6'compile 'redis.clients:jedis:2.9.0'compile 'org.apache.logging.log4j:log4j-core:2.8.2'compile 'org.apache.commons:commons-pool2:2.4.2'compile 'com.univocity:univocity-parsers:2.8.2'
}

3.2 核心流程代码

将vote.csv文件改名为googlemapvotes.csv,并将其置于资源目录下。
       先定义一个csv行数据的java bean类:

public class CsvPanoBean {private String panoId;private double lati;private double lonti;public CsvPanoBean(String panoId,double lati, double lonti){this.panoId = panoId;this.lati = lati;this.lonti = lonti;}public String getPanoId() {return panoId;}public void setPanoId(String panoId) {this.panoId = panoId;}public double getLati() {return lati;}public void setLati(double lati) {this.lati = lati;}public double getLonti() {return lonti;}public void setLonti(double lonti) {this.lonti = lonti;}
}

编写核心代码,含义详见注释:

protected boolean process() {String filePath = "/googlemapvotes.csv";// 创建csv解析器settings配置对象CsvParserSettings settings = new CsvParserSettings();// 文件中使用 '\n' 作为行分隔符// 确保像MacOS和Windows这样的系统// 也可以正确处理(MacOS使用'\r';Windows使用'\r\n')settings.getFormat().setLineSeparator("\n");// 考虑文件中的第一行内容解析为列标题,跳过第一行settings.setHeaderExtractionEnabled(true);// 创建CSV解析器(将分隔符传入对象)CsvParser parser = new CsvParser(settings);// 调用beginParsing逐个读取记录,使用迭代器iteratorparser.beginParsing(getReader(filePath));String[] row;//图片下载工具类PicLoadUtils picLoadUtils = new PicLoadUtils();//创建线程池,由于本地机器为8核CPU,故定义10个核心线程,最大线程数为16,且自定义线程工厂类和饱和策略ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 16, 100, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(1024), new MyTreadFactory(),  new MyIgnorePolicy());//预启动所有核心线程executor.prestartAllCoreThreads();//解析csv文件并迭代每行记录while ((row = parser.parseNext()) != null) {String category = row[7];//这里根据需求,优先下载safety类型的训练集街景图片if ("safety".equals(category)){String leftPanoId = row[0];String rightPanoId = row[1];double leftLati = Double.parseDouble(row[3]);double leftLonti = Double.parseDouble(row[4]);double rightLati = Double.parseDouble(row[5]);double rightLonti = Double.parseDouble(row[6]);CsvPanoBean leftPanoBean = new CsvPanoBean(leftPanoId,leftLati,leftLonti);CsvPanoBean rightPanoBean = new CsvPanoBean(rightPanoId,rightLati,rightLonti);CsvPanoBean[] csvPanoBeans = {leftPanoBean,rightPanoBean};for (CsvPanoBean element:csvPanoBeans){//判断redis中或本地是否有该街景IDString panoId = element.getPanoId();//boolean isExists = isPicExists(panoId);boolean isExists = redisUtils.isPanoIDExists(panoId);if (!isExists){redisUtils.panoIdPush(panoId);DownloadPicTask task = new DownloadPicTask(picLoadUtils,element);executor.execute(task);}else{logger.info(panoId + " is exist");}}try {// 这里主线程需要睡一会,否则容易引起多线程下载时的读超时Thread.sleep(400L);logger.info("The queue size of Thread Pool is "+ executor.getQueue().size());}catch (InterruptedException e){e.printStackTrace();}}}logger.info("--------------------------crawl finished!--------------------------");// 在读取结束时自动关闭所有资源,或者当错误发生时,可以在任何使用调用stopParsing()// 只有在不是读取所有内容的情况下调用下面方法,但如果不调用也没有非常严重的问题parser.stopParsing();isComplete = true;return true;
}//读文件时定义编码格式
private Reader getReader(String relativePath) {try {return new InputStreamReader(this.getClass().getResourceAsStream(relativePath), "UTF-8");} catch (UnsupportedEncodingException e) {throw new IllegalStateException("Unable to read input", e);}
}//判断本地是否已存在
private boolean isPicExists(String panoId){String path = "E:\\temp\\hgg-googlemap\\safety\\"+panoId+".jpg";File file = new File(path);return file.exists();
}

3.3 图片下载工具类

该工具作用:主要是下载路径的设置及下载图片时的检测

/*** @author Huigen Zhang* @since 2018-10-19 18:53**/
public class PicLoadUtils {private final static String WINDOWS_DISK_SYMBOL = ":";private final static String WINDOWS_PATH_SYMBOL = "\\";private final static int STATUS_CODE = 200;private String localLocation;{//要下载到本地的路径localLocation = this.getFileLocation("googlepano");}private String getFileLocation(String storeDirName){String separator = "/";ConfigParser parser = ConfigParser.getInstance();String spiderId = "spider-googlemap";SpiderConfig spiderConfig = new SpiderConfig(spiderId);Map<String,Object> storageConfig = (Map<String, Object>) parser.assertKey(spiderConfig.getSpiderConfig(),"storage", spiderConfig.getConfigPath());String fileLocation = (String) parser.getValue(storageConfig,"piclocation",null,spiderConfig.getConfigPath()+".storage");String pathSeparator = getSeparator();String location;if(fileLocation!=null){//先区分系统环境,再判断是否为绝对路径if (separator.equals(pathSeparator)){//linuxif(fileLocation.startsWith(separator)){location = fileLocation + pathSeparator + "data";}else {location = System.getProperty("user.dir") + pathSeparator + fileLocation;}location = location.replace("//", pathSeparator);return location;}else {//windowsif (fileLocation.contains(WINDOWS_DISK_SYMBOL)){//绝对路径location = fileLocation + pathSeparator + "data";}else {//相对路径location = System.getProperty("user.dir") + pathSeparator + fileLocation;}location = location.replace("\\\\",pathSeparator);}}else{//默认地址location = System.getProperty("user.dir") + pathSeparator + storeDirName;}return location;}private String getSeparator(){String pathSeparator = File.separator;if(!WINDOWS_PATH_SYMBOL.equals(File.separator)){pathSeparator = "/";}return pathSeparator;}private void mkDir(File file){String directory = file.getParent();File myDirectory = new File(directory);if (!myDirectory.exists()) {myDirectory.mkdirs();}}public boolean downloadPic(String url, String panoId){okhttp3.Request request = new okhttp3.Request.Builder().url(url).build();Response response = null;InputStream inputStream = null;FileOutputStream out = null;String relativePath;try {response = OkHttpUtils.getInstance().newCall(request).execute();if (response.code()!=STATUS_CODE){return false;}//将响应数据转化为输入流数据inputStream = response.body().byteStream();byte[] buffer = new byte[2048];relativePath = panoId + ".jpg";File myPath = new File(localLocation + File.separator + relativePath);this.mkDir(myPath);out = new FileOutputStream(myPath);int len;while ((len = inputStream.read(buffer)) != -1){out.write(buffer,0,len);}//刷新文件流out.flush();} catch (IOException e) {e.printStackTrace();}finally {if (inputStream!=null){try {inputStream.close();}catch (IOException e){e.printStackTrace();}}if (null!=out){try {out.close();}catch (IOException e){e.printStackTrace();}}if (null!=response){response.body().close();}}return true;}
}

3.4 redis工具类

主要还是运用了上述redis命令,在这基础上做一层封装:

/*** @author zhanghuigen* @since 0.1.0**/
public class RedisUtils {private JedisPool pool;private String spiderUUID;private static Logger logger = Logger.getLogger(RedisUtils.class);public RedisUtils(String host, int port, String password, String spiderUUID) {this(new JedisPool(new JedisPoolConfig(), host, port, 2000, password));this.spiderUUID = spiderUUID;}public RedisUtils(JedisPool pool) {this.pool = pool;}public synchronized Boolean isPanoIDExists(String panoId) {Jedis jedis = null;Boolean exists;try {jedis = this.pool.getResource();exists = jedis.exists(this.spiderUUID + ":" + panoId);return exists;}finally {if (jedis!=null){jedis.close();}}}public synchronized boolean removeKeys(){Jedis jedis = this.pool.getResource();try {Set<String> keys = jedis.keys(this.spiderUUID + ":*" );if(keys != null && !keys.isEmpty()) {logger.info("redis has stored " + keys.size() + " keys, now ready to remove them all!");String[] array = new String[keys.size()];jedis.del(keys.toArray(array));}return true;}catch (Exception e){e.printStackTrace();}finally {if (jedis!=null){jedis.close();}}return true;}public synchronized boolean panoIdPush(String panoId) {Jedis jedis = this.pool.getResource();try {long num = jedis.setnx(this.spiderUUID + ":" + panoId, String.valueOf(1));return num==1;} finally {if (jedis!=null){jedis.close();}}}
}

3.5 线程池的任务类及拒绝策略

这里其实也可以运用Callable+Future的模式定义下载任务,详见: https://www.cnblogs.com/hapjin/p/7599189.html 或 https://www.cnblogs.com/myxcf/p/9959870.html

class DownloadPicTask implements Runnable {private CsvPanoBean taskBean;private PicLoadUtils picLoadUtils;private String panoId;private DownloadPicTask(PicLoadUtils picLoadUtils,CsvPanoBean bean) {this.picLoadUtils = picLoadUtils;this.taskBean = bean;this.panoId = taskBean.getPanoId();}@Overridepublic void run() {logger.info("正在执行task "+panoId);String url;String key;boolean successDownload;do {//拼接街景图片urlString[] urlWithKey = getUrlWithKey(taskBean);url = urlWithKey[0];key = urlWithKey[1];//发送请求,下载图片,直到本图片下载成功为止successDownload = picLoadUtils.downloadPic(url,panoId);}while (!successDownload);logger.info(panoId + " downloaded succeed with " + key);}@Overridepublic String toString(){return panoId;}private String[] getUrlWithKey(){String requestPrefix = "https://maps.googleapis.com/maps/api/streetview?size=400x300&location=";String url = requestPrefix + taskBean.getLati() + "," + taskBean.getLonti() + "&key=";Random random = new Random();//这里需确保可用的key已经配置在配置文件中,并已读取至一个List----googleKeys中int index = random.nextInt(5);String key = googleKeys.get(index);return new String[]{url+key,key};}
}class MyTreadFactory implements ThreadFactory {private final AtomicInteger mThreadNum = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());logger.info(t.getName() + " has been created");return t;}
}class MyIgnorePolicy implements RejectedExecutionHandler {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) {doLog(r, e);}private void doLog(Runnable r, ThreadPoolExecutor e) {// 将拒绝执行的街景ID写入日志logger.warn( r.toString() + " rejected");}
}

4 写在最后

  • 单线程与多线程下载的效率比较

若用单线程下载,差不多1秒一张图片,相对低效:

       采用线程池后,刚开始线程数量设的较高,也没有在主线程中加入睡眠时间,易出现读超时现象,原因是使用公司代理访问google时,多线程下载使得带宽受限。引起线程迟迟读不到数据后报异常,如下图所示:

       通过在主线程添加睡眠时间后,读超时现象消失,可以顺利下载:

       在满足带宽条件下,下载速度约5张/秒,

  • 正常运行时的本地效果图
  • 图片质量检测
           实际上,该训练集中有部分图片因google资源缺失无法下载。

           解决方法:可以提前在下载过程中进行检测,一般此类图片size较小,可以通过在图片下载工具类中对下载返回的响应加个判断来决定是否对其下载,并记录好异常位置即可。

MIT Place Pulse数据集及google街景图片爬取相关推荐

  1. 爬虫——百度图片爬取

    通用爬虫和聚焦爬虫 根据使用场景,网络爬虫可分为 通用爬虫 和 聚焦爬虫 两种. 通用网络爬虫:是捜索引擎抓取系统(Baidu.Google.Yahoo等)的重要组成部分,是从互联网中搜集网页.采集信 ...

  2. Python 爬虫 爬取豆瓣Top 250 并将海报图片爬取下来保存

    本文章的所有代码和相关文章, 仅用于经验技术交流分享,禁止将相关技术应用到不正当途径,滥用技术产生的风险与本人无关. 本文章是自己学习的一些记录. 爬取豆瓣top 250 现在的很多学习的教程例子都是 ...

  3. Python图片爬取方法总结

    1. 最常见爬取图片方法 对于图片爬取,最容易想到的是通过urllib库或者requests库实现.具体两种方法的实现如下: 1.1 urllib 使用urllib.request.urlretrie ...

  4. python爬虫图片-Python图片爬取方法总结

    1. 最常见爬取图片方法 对于图片爬取,最容易想到的是通过urllib库或者requests库实现.具体两种方法的实现如下: 1.1 urllib 使用urllib.request.urlretrie ...

  5. 图片爬取数据解析数据持久化

    文章目录 1.图片下载 2.JS动态渲染 3.数据解析 4.持久化存储 1.图片下载 百度图片:http://image.baidu.com/ 搜狗图片:https://pic.sogou.com/ ...

  6. Python爬虫入门教程 26-100 知乎文章图片爬取器之二

    1. 知乎文章图片爬取器之二博客背景 昨天写了知乎文章图片爬取器的一部分代码,针对知乎问题的答案json进行了数据抓取,博客中出现了部分写死的内容,今天把那部分信息调整完毕,并且将图片下载完善到代码中 ...

  7. python爬虫知乎图片_Python爬虫入门教程 25-100 知乎文章图片爬取器之一

    1. 知乎文章图片爬取器之一写在前面 今天开始尝试爬取一下知乎,看一下这个网站都有什么好玩的内容可以爬取到,可能断断续续会写几篇文章,今天首先爬取最简单的,单一文章的所有回答,爬取这个没有什么难度. ...

  8. java spring+mybatis整合实现爬虫之《今日头条》搞笑动态图片爬取

    java spring+mybatis整合实现爬虫之<今日头条>搞笑动态图片爬取(详细) 原文地址原博客地址 先上效果图 抓取的动态图: 数据库: 一.此爬虫介绍 今日头条本身就是做爬虫的 ...

  9. 图片爬取和IP地址查询

    图片爬取: import requests import os url="https://img.alicdn.com/imgextra/i2/2208313525338/O1CN01qj2 ...

  10. Python爬虫之scrapy框架360全网图片爬取

    Python爬虫之scrapy框架360全网图片爬取 在这里先祝贺大家程序员节快乐,在此我也有一个好消息送给大家,本人已开通了微信公众号,我会把资源放在公众号上,还请大家小手动一动,关注过微信公众号, ...

最新文章

  1. ATL CLR MFC Win32 常规 的区别
  2. urllib2设置代理
  3. SAP Spartacus 4.0 源代码模式下开启 SSR,为什么会从本地去加载 all.css?
  4. android中文api(85)——HorizontalScrollView
  5. 不安装cudnn可不可以_Linux非root用户如何优雅的安装cuda和cudnn
  6. 在 IIS 中部署 SPA 应用,多么痛的领悟!
  7. #USB加密狗信息安全与USB_Host 硬件读写加密狗
  8. 软考程序员Java答题速成_软考程序员考试下午题解答方法与技巧
  9. 实对称矩阵性质的数学证明
  10. winhex万能恢复磁盘数据
  11. 图形图像基础 之 gif介绍
  12. android调试更换模拟器,Android建立模拟器进行调试(示例代码)
  13. 国防科技大学计算机考研资料汇总
  14. 苹果新产品中的机器学习算法
  15. 腾讯企业邮箱解析到阿里云域名
  16. DevpTips_JupyterNotebook的基本命令IPython
  17. python 将JS(JavaScript)的json格式字符串转换为python的字典格式
  18. 美国第一个设置计算机学科的学校,美国普渡大学希拉法叶校区
  19. 记录本地 Docker 新建镜像,推送到Daocloud
  20. 使用 satis 搭建一个私有的 Composer 包仓库 在我们的日常php开发中可能需要使用大量的composer包,大部份都可以直接使用,但在公司内部总有一小部份包是不能公开的,这时候我们就需

热门文章

  1. c++ 求点到直线的距离
  2. JAVA-工作流-Activiti7入门demo
  3. 51单片机入门——DS18B20
  4. MSP430单片机与SIM800A调试
  5. 高通QCC3020应用开发的软件平台的搭建
  6. JavaScript模块化编程(CommonJS篇)
  7. cxf框架Demo1
  8. java课程设计报告书_java课程设计报告书模板
  9. STM32编译生成的BIN文件详解
  10. C++调用VSS API进行快照