MIT Place Pulse数据集及google街景图片爬取

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逐个读取记录,使用迭代器iterator
    parser.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中或本地是否有该街景ID
                String 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)){
                //linux
                if(fileLocation.startsWith(separator)){
                    location = fileLocation + pathSeparator + "data";
                }else {
                    location = System.getProperty("user.dir") + pathSeparator + fileLocation;
                }
                location = location.replace("//", pathSeparator);
                return location;
            }else {
                //windows
                if (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();
    }

    @Override
    public void run() {
        logger.info("正在执行task "+panoId);
        String url;
        String key;
        boolean successDownload;
        do {
            //拼接街景图片url
            String[] urlWithKey = getUrlWithKey(taskBean);
            url = urlWithKey[0];
            key = urlWithKey[1];
            //发送请求,下载图片,直到本图片下载成功为止
            successDownload = picLoadUtils.downloadPic(url,panoId);
        }while (!successDownload);
        logger.info(panoId + " downloaded succeed with " + key);
    }

    @Override
    public 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);
    @Override
    public 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 {

    @Override
    public 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较小,可以通过在图片下载工具类中对下载返回的响应加个判断来决定是否对其下载,并记录好异常位置即可。
  • 7
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 38
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值