深入JAVA并发编程(七):并发容器(三)

并发容器

ConcurrentSkipListMap

我们知道HashMap是一种键值对形式的数据存储容器,它内部的元素是无序的。当然了,JDK也提供了TreeMap,TreeMap使用红黑树按照key的顺序(自然顺序、自定义顺序)来使得键值对有序存储,但是和HashMap同样是线程不安全的,因此在JAVA并发包中提供了ConcurrentSkipListMap容器,它能够保证在多线程环境下使键值对按照key的顺序来存储。

ConcurrentSkipListMap的底层是通过跳表来实现的,跳表(SkipList) 是一种随机化的数据结构, 通过“空间来换取时间”的一个算法,建立多级索引来进行查找, 时间复杂度和红黑树一样为O(log n),实现却比红黑树简单的多。为了更清晰的了解这种数据结构,我们画图来讲解一下。

跳表的数据结构

首先我们普通的链表如下图所示

在这里插入图片描述

我们知道链表这种数据结构查找起来是非常麻烦的,往往需要通过遍历的形式,效率非常之低,如上图的链表,如果我们查询9,21,30这三个元素,那么就需要依次遍历分别需要比较3,6,8次,当元素比较多时,这个过程就会非常耗时,而跳表的解决思路是怎样的呢?跳表会从链表中挑选出一些元素作为比较的索引,如下图
在这里插入图片描述

这样呢我们就可以先与上层的索引进行比较,就可以跳过一些不必要的比较来提高效率。

SkipList具备如下特性:

  • 由很多层结构组成,要不要新加层完全是由一个随机的算法来决定的
  • 每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法
  • 最底层(Level 1)的链表包含所有元素
  • 如果一个元素出现在Level i层的链表中,则它在Level i层之下的链表也都会出现

如下,就是一个典型的跳表结构

在这里插入图片描述

基于上图,例如我们要查找元素21的话

首先从最高层开始比较,比较3,比3大,然后比较9,比9大,然后比较25,比25小,然后从9的下一层开始查找,比较16,比16大,又比较25,比25小,又从下一层找,直到找到21.

在这里插入图片描述

这个过程元素少的时候不明显,如果元素很多,就能看到性能显著的提升。

然后我们来说下插入操作,首先需要查找合适的位置。并且在确认新节点要占据的层次K时,是完全随机的。如果占据的层次K大于链表的层次,则重新申请新的层,否则插入指定层次。

例如我们要插入一个23这个元素,那么如果随机生成的k>3时,则需要申请新的层

在这里插入图片描述

如果k=2,则如下,这个过程是完全随机的。

在这里插入图片描述

删除操作很简单,找到节点,删除节点,调整指针即可,但是ConcurrentSkipListMap删除操作还有一个很特别的地方,后面我们会从源码分析。

源码分析
首先我们来看下类中的重要属性。

我们先来看下Node节点类 。

Node的结构和一般的单链表节点毫无区别,key-value和一个指向下一个节点的next。是最基本的存储单元

    static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;

       	
        Node(K key, Object value, Node<K,V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
        //省略
        .....
    }

Index是一个基于Node节点的索引Node,里面一个指向下一个Index的right,一个指向下层的down节点。

    static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;

        /**
         * Creates index node with given values.
         */
        Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
            this.node = node;
            this.down = down;
            this.right = right;
        }
        //省略
    }

HeadIndex类封装了Index类,作为每层的头结点,有一个level来定义层级。

    static final class HeadIndex<K,V> extends Index<K,V> {
        final int level;
        HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
            super(node, down, right);
            this.level = level;
        }
    }

接下来是一些重要属性

    //头节点指向的Node类
    private static final Object BASE_HEADER = new Object();

    //整个跳表的头结点,通过它可以遍历访问整张跳表。
    private transient volatile HeadIndex<K,V> head;

    //比较器
    final Comparator<? super K> comparator;

接下来我们来看下该类的构造方法

    public ConcurrentSkipListMap() {
        this.comparator = null;
        initialize();
    }

    public ConcurrentSkipListMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
        initialize();
    }


    public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
        this.comparator = null;
        initialize();
        putAll(m);
    }

    public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
        this.comparator = m.comparator();
        initialize();
        buildFromSorted(m);
    }

我们发现它们都调用了initialize方法来初始化

    private void initialize() {
        keySet = null;
        entrySet = null;
        values = null;
        descendingMap = null;
        //初始化头节点
        head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
                                  null, null, 1);
    }

接下来就是重中之重了, 我们来看下插入元素的put方法,过程比较复杂。

    public V put(K key, V value) {
    	//值不能为null
        if (value == null)
            throw new NullPointerException();
        return doPut(key, value, false);
    }
    private V doPut(K key, V value, boolean onlyIfAbsent) {
        Node<K,V> z;             // added node
        //判断key是否为null
        if (key == null)
            throw new NullPointerException();
        //获取比较器
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
        	//根据 key,找到待插入的位置
        	//b为前驱节点,将来作为新加入结点的前驱节点
        	//n为后继结点,将来作为新加入结点的后继结点
        	//新节点将插入在 b 和 n 之间
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            	//如果n为 null,那么说明b是链表的尾节点,这种情况比较简单,直接构建新节点插入即可
            	//如果不为null则执行下面代码
                if (n != null) {
                    Object v; int c;
                    //获取n的后继节点
                    Node<K,V> f = n.next;
                    //如果n不再是b的后继结点,说明在此期间有其他线程向b后面添加了新元素
                	//那么我们直接退出循环,重新计算新节点将要插入的位置
                    if (n != b.next)               // inconsistent read
                        break;
                    //value =null说明n已经被标识为待删除,其他线程正在进行删除操作
                    //在后面删除源码时会讲到
                	//调用helpDelete帮助删除,并退出循环重新计算待插入位置
                    if ((v = n.value) == null) {   // n is deleted
                        n.helpDelete(b, f);
                        break;
                    }
                    //如果b.value为空,说明前驱节点b已经被删除了,退出循环重新计算插入位置
                    if (b.value == null || v == n) // b is deleted
                        break;
                    //如果新节点的key>n.key,说明找到的前驱节点有误
                    //按顺序往后挪一个位置,退出当前循环,重新比较
                    if ((c = cpr(cmp, key, n.key)) > 0) {
                        b = n;
                        n = f;
                        continue;
                    }
                    //新节点的 key 等于 n 的 key,CAS 更新即可
                    if (c == 0) {
                        if (onlyIfAbsent || n.casValue(v, value)) {
                            @SuppressWarnings("unchecked") V vv = (V)v;
                            return vv;
                        }
                        break; // restart if lost race to replace value
                    }
                    // else c < 0; fall through
                }
				//构建新的节点
                z = new Node<K,V>(key, value, n);
                //CAS插入
                if (!b.casNext(n, z))
                    break;         // restart if lost race to append to b
                break outer;
            }
        }
        //上面做的操作就是将元素插入到最底层的level
        //接下来要做的就是生成随机level层,来决定这个元素在哪些层存在
		//首先获取一个随机数,四个字节,32位
        int rnd = ThreadLocalRandom.nextSecondarySeed();
        //和 1000 0000 0000 0000 0000 0000 0000 0001 进行与运算
        //如果不等于 0,那么将仅仅把新节点插入到最底层的链表中即可,不会往上层插入
    	//如果等于 0,说明这个随机数最高位和最低位都为 0,这种概率很大
        if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
            int level = 1, max;
            //用低位连续为 1 的个数作为 level 的值,随机策略
            while (((rnd >>>= 1) & 1) != 0)
                ++level;
            Index<K,V> idx = null;
            HeadIndex<K,V> h = head;
            //如果得到的level 在当前跳表 level 范围内
        	//构建一个从 1 到 level 的纵列 index 结点链表
            if (level <= (max = h.level)) {
                for (int i = 1; i <= level; ++i)
                    idx = new Index<K,V>(z, idx, null);
            }
            //否则需要新增一个 level 层
            else { // try to grow by one level
                level = max + 1; // hold in array and later pick the one to use
                //创建一个index节点数组
                @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                    (Index<K,V>[])new Index<?,?>[level+1];
                //构建一个从 1 到 level 的纵列 index 结点链表
                for (int i = 1; i <= level; ++i)
                    idxs[i] = idx = new Index<K,V>(z, idx, null);
                //更新头节点
                for (;;) {
                    h = head;
                    int oldLevel = h.level;
                    //level 肯定是比 oldLevel 大一的,如果小了说明其他线程更新过表了
                    if (level <= oldLevel) // lost race to add level
                        break;
                    HeadIndex<K,V> newh = h;
                    Node<K,V> oldbase = h.node;
                    //更新头指针
                    for (int j = oldLevel+1; j <= level; ++j)
                        newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                    if (casHead(h, newh)) {
                        h = newh;
                        idx = idxs[level = oldLevel];
                        break;
                    }
                }
            }
            // 上面代码做的只是创建level个节点并且纵向关联,但是横向并没有关联,接下来要做的就是横向关联
            splice: for (int insertionLevel = level;;) {
                int j = h.level;
                for (Index<K,V> q = h, r = q.right, t = idx;;) {
                	//其他线程并发操作导致头结点被删除,直接退出外层循环
                    if (q == null || t == null)
                        break splice;
                    
                    if (r != null) {
                        Node<K,V> n = r.node;
                        // compare before deletion check avoids needing recheck
                        int c = cpr(cmp, key, n.key);
                        //如果 n 正在被其他线程删除,那么调用 unlink 去删除它
                        if (n.value == null) {
                            if (!q.unlink(r))
                                break;
                            //重新获取q的右节点,再次进入循环
                            r = q.right;
                            continue;
                        }
                        //c > 0 说明前驱结点定位有误,重新进入
                        if (c > 0) {
                            q = r;
                            r = r.right;
                            continue;
                        }
                    }
					
                    if (j == insertionLevel) {
                    	//尝试着将 t 插在 q 和 r 之间,如果失败了,退出内循环重试
                        if (!q.link(r, t))
                            break; // restart
                        //如果插入完成后,t 结点被删除了,那么结束插入操作
                        if (t.node.value == null) {
                            findNode(key);
                            break splice;
                        }
                        // 标志的插入层 -- ,如果== 0 ,表示已经到底了,插入完毕,退出循环
                        if (--insertionLevel == 0)
                            break splice;
                    }
					// 上面节点已经插入完毕了,插入下一个节点
                    if (--j >= insertionLevel && j < level)
                        t = t.down;
                    q = q.down;
                    r = q.right;
                }
            }
        }
        return null;
    }

我们画图来讲解一下这个过程,首先初始化一个ConcurrentSkipListMap如下图所示:

在这里插入图片描述

添加 key=1, value = A ,key=2,value=B节点,寻找前继节点, 这时返回的 b = BaseHeader, n = null,所以直接插入,然后假设获取的 level 是0(要知道获得0的概率是很大的, 这个函数返回的最大值也就31, 也就是说, 最多有31层的索引),idx = null, 直接break 出去,操作结束

在这里插入图片描述

接下来再添加key=3,Value=C节点,寻找前继节点, 这时返回的 b = node2, n = null,所以直接插入。这时我们假设我们获取到 level = 1, 则步骤14 中 level <= max(max = 1)成立,初始化一个 idx,最终找到要插入index位置, 进行link操作,此时情况如下:

在这里插入图片描述

然后我们来添加key=4,Value=D,这个和上面一样,直接插入在后面即可,然后我们来插入key=5,value=e这个节点,首先寻找前继节点, 这时返回的 b = node2, n = null,所以直接插入,然后假设我们获取到 level = 25, 则 level <= max(max = 1)不成立,所以这时候我们就要多加一层了,首先进行idx 链表的初始化, 一共两个链表节点 (idx是纵向的链表),然后增加一层HeadIndex,

在这里插入图片描述

其实doPut方法就是获取key对应的前继节点, 然后cas设置next值, 随后 生成随机 level(0-31之间), 若新的 level <= oldMaxLevel 则增加对应的索引层, 若level > oldMaxLevel, 则 HeadIndex 也会随之增加索引层;

接下来我们来看看该方法中调用的几个重要方法。

首先看一下寻找前驱节点的方法,思路是从矩形链表的左上角的 HeadIndex 索引开始, 先向右, 遇到 null, 或 > key 时向下, 重复向右向下找, 一直找到 对应的前继节点(前继节点就是小于 key 的最大节点)

    private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
        if (key == null)
            throw new NullPointerException(); // don't postpone errors
        for (;;) {
        	//q是最顶层HeadIndex,r为它的右驱节点
            for (Index<K,V> q = head, r = q.right, d;;) {
            	//如果它的右驱节点不为空
                if (r != null) {
                	//获取它内部的node
                    Node<K,V> n = r.node;
                    //获取node的key值
                    K k = n.key;
                    //如果值为null,说明该节点正在被删除,帮助删除,然后再次获取HeadIndex新的右驱节点,重新从头执行
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;           // restart
                        r = q.right;         // reread r
                        continue;
                    }
                    //如果当前key>右驱节点的key,则向右遍历,向右移动一位继续从头比较
                    if (cpr(cmp, key, k) > 0) {
                        q = r;
                        r = r.right;
                        continue;
                    }
                }
                //如果q.down==null,则说明已经到底层
                //并且此时q.key< key < r.key了,所以直接返回前驱节点q.node
                if ((d = q.down) == null)
                    return q.node;
                //向下走
                q = d;
                r = d.right;
            }
        }
    }

接下来我们来看看get方法获取值的方法

    private V doGet(Object key) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
        	//获取key的前驱节点,此时应该是n.key>=key的
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                //如果n不存在,则说明这个key对应的node不存在,直接返回null
                if (n == null)
                    break outer;
                Node<K,V> f = n.next;
                //如果n!=b.next,说明有其他线程执行了插入操作,重新从头执行
                if (n != b.next)                // inconsistent read
                    break;
                //n.value==null说明n已经被删除了,重新从头执行
                if ((v = n.value) == null) {    // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                //说明前驱节点已经被删除了,重新从头执行
                if (b.value == null || v == n)  // b is deleted
                    break;
                //如果key和n.key相同,则返回n.value
                if ((c = cpr(cmp, key, n.key)) == 0) {
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }
                //如果c<0,则说明key<n.key,说明不存在key对应的node
                if (c < 0)
                    break outer;
                //走到这儿说明当前位置不准确,向后移动一位重新从头开始比较
                b = n;
                n = f;
            }
        }
        return null;
    }

然后我们来看下删除方法

    final V doRemove(Object key, Object value) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
        	//获取key的前驱节点
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                //如果n=null,则说明不存在该key对应的元素,直接返回
                if (n == null)
                    break outer;
                Node<K,V> f = n.next;
                //如果n!=b.next,说明有其他线程添加元素了,重新开始执行
                if (n != b.next)                    // inconsistent read
                    break;
                //如果n.value=null,说明n正在被删除,所以重新开始执行
                if ((v = n.value) == null) {        // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                //说明前驱节点b被删除了,重新开始执行
                if (b.value == null || v == n)      // b is deleted
                    break;
                //如果key<n.key,则说明不存在该key对应的元素,直接返回
                if ((c = cpr(cmp, key, n.key)) < 0)
                    break outer;
                //如果key>n.key,说明当前定位有偏差,向右移动一位,再继续执行
                if (c > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                //value传进来就是null,所以这个跳过
                if (value != null && !value.equals(v))
                    break outer;
                //将n的值设置为null,进行数据删除
                if (!n.casValue(v, null))
                    break;
                //这里会在n的后面添加一个Marker节点,将b和n的后继节点f链接,然后删除n
                if (!n.appendMarker(f) || !b.casNext(n, f))
                	//对 key 对应的index 进行删除
                    findNode(key);                  // retry via findNode
                //对 key 对应的index 进行删除 上一步进行操作失败后通过 findPredecessor 进行index 的删除
                else {
                    findPredecessor(key, cmp);      // clean index
                     //进行headIndex 对应的index 层的删除
                    if (head.right == null)
                        tryReduceLevel();
                }
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
        }
        return null;
    }

这里有个比较特别的地方,我们发现在删除的时候,会先在被删除的节点后面增加一个Marker节点。那么为什么要多此一举做这步操作呢?主要是为了防止误删除的情况发生,我们来看个例子:

例如有三个连续的节点ABC,线程1准备在B节点的后面插入一个节点 D, 它先判断 B.value == null, 发现 B 没被删除,这时线程2对B进行删除,线程2设置next为节点C成功删除B,这时候线程1设置B节点的后继节点为D,但是因为节点B被删除了,导致节点D虽然插入成功但是却找不到了。

但是如果增加了Marker节点就能很好的解决这个问题。
还是有三个连续的节点ABC,线程1准备在B节点的后面插入一个节点 D, 它先判断 B.value == null, 发现 B 没被删除,线程2对B节点进行删除,在B的后面增加一个MarkerNode M,然后线程2将节点B和M一起删除。这时候在线程1插入节点D过程中会出现如下几种情况:

  • (1)在线程2设置 marker节点前操作, 然后线程1执行B.casNext(C, D) 成功, 什么都未影响
  • (2) 在线程2设置maker之后, 删除 b与marker之前, 这时线程1执行B.casNext(C, D) 操作失败(因为b.next 变成maker了)
  • (3)在线程2删除 B, marker 之后, 线程1执行B.casNext(C, D) 失败(因为b.next变成maker, B.casNext(C,D) 操作失败)

maker 节点的存在致使非阻塞链表能实现中间节点的删除和插入同时安全进行(反过来就是若没有marker节点, 有可能刚刚插入的数据就丢掉了)。

除此之外还有一个并发容器为ConcurrentSkipListSet,他内部完全是使用的ConcurrentSkipListMap来实现,这里就不再细说。

【JUC】JDK1.8源码分析之ConcurrentSkipListMap

ConcurrentSkipListMap 源码分析 (基于Java 8)

Fork/Join框架

Fork/Join框架是JDK1.7中提供的用于并行执行任务的框架,如名字所示,它的思想首先是Fork,把一个大任务分割成若干个小任务,然后进行Join,汇总每个小任务的结果得到最终任务结果。运行流程图如下:

在这里插入图片描述

上述过程Fork/Join框架提供了两个类让我们完成上述事情。

  • ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务,它提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接使用ForkJoinTask,而是使用它的子类。RecursiveAction类用于没有返回结果的任务。RecursiveTask用于有返回结果的任务。
  • ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。

使用案例

我们先通过一个例子来看一下如何使用,计算1-1000的值。

public class ForkJoinDemo  extends RecursiveTask<Integer>{
	//阀值
    private static final int THRESHOLD=100;
    //起始结束值
    private int start;
    private int end;

    public ForkJoinDemo(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum=0;
        //如果条件成立,说明任务已经被分的足够小,开始执行计算逻辑
        if(end-start<=THRESHOLD){
            System.out.println("开始计算的部分:"+start+"-"+end);
            for (int i=start;i<=end;i++){
                sum+=i;
            }
        //否则,说明任务太大,将任务分割后执行
        }else {
            ForkJoinDemo subTask1=new ForkJoinDemo(start,(start+end)/2);
            ForkJoinDemo subTask2=new ForkJoinDemo((start+end)/2+1,end);
            subTask1.fork();
            subTask2.fork();
            sum=subTask1.join()+subTask2.join();
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinDemo forkJoinDemo=new ForkJoinDemo(1,1000);
        ForkJoinPool forkJoinPool=new ForkJoinPool();
        ForkJoinTask<Integer> submit = forkJoinPool.submit(forkJoinDemo);
        Integer result = null;
        try {
            result = submit.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(result);
    }
}

在这里插入图片描述

ForkJoinTask需要实现compute方法,在这个方法里,首先要判断任务是否足够小,如果足够小就直接执行任务,否则就需要分割成两个子任务,然后每个子任务调用fork方法时,又会进入compute方法进行判断。使用join方法会等待子任务执行完并得到其结果。

ForkJoinPool内部由ForkJoinTask数组和ForkJoinWorkerThread数组组成。ForkJoinTask数组负责存放用户提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组则负责执行这些任务。

在使用Python来安装geopandas包时,由于geopandas依赖于几个其他的Python库(如GDAL, Fiona, Pyproj, Shapely等),因此安装过程可能需要一些额外的步骤。以下是一个基本的安装指南,适用于大多数用户: 使用pip安装 确保Python和pip已安装: 首先,确保你的计算机上已安装了Python和pip。pip是Python的包管理工具,用于安装和管理Python包。 安装依赖库: 由于geopandas依赖于GDAL, Fiona, Pyproj, Shapely等库,你可能需要先安装这些库。通常,你可以通过pip直接安装这些库,但有时候可能需要从其他源下载预编译的二进制包(wheel文件),特别是GDAL和Fiona,因为它们可能包含一些系统级的依赖。 bash pip install GDAL Fiona Pyproj Shapely 注意:在某些系统上,直接使用pip安装GDAL和Fiona可能会遇到问题,因为它们需要编译一些C/C++代码。如果遇到问题,你可以考虑使用conda(一个Python包、依赖和环境管理器)来安装这些库,或者从Unofficial Windows Binaries for Python Extension Packages这样的网站下载预编译的wheel文件。 安装geopandas: 在安装了所有依赖库之后,你可以使用pip来安装geopandas。 bash pip install geopandas 使用conda安装 如果你正在使用conda作为你的Python包管理器,那么安装geopandas和它的依赖可能会更简单一些。 创建一个新的conda环境(可选,但推荐): bash conda create -n geoenv python=3.x anaconda conda activate geoenv 其中3.x是你希望使用的Python版本。 安装geopandas: 使用conda-forge频道来安装geopandas,因为它提供了许多地理空间相关的包。 bash conda install -c conda-forge geopandas 这条命令会自动安装geopandas及其所有依赖。 注意事项 如果你在安装过程中遇到任何问题,比如编译错误或依赖问题,请检查你的Python版本和pip/conda的版本是否是最新的,或者尝试在不同的环境中安装。 某些库(如GDAL)可能需要额外的系统级依赖,如地理空间库(如PROJ和GEOS)。这些依赖可能需要单独安装,具体取决于你的操作系统。 如果你在Windows上遇到问题,并且pip安装失败,尝试从Unofficial Windows Binaries for Python Extension Packages网站下载相应的wheel文件,并使用pip进行安装。 脚本示例 虽然你的问题主要是关于如何安装geopandas,但如果你想要一个Python脚本来重命名文件夹下的文件,在原始名字前面加上字符串"geopandas",以下是一个简单的示例: python import os # 指定文件夹路径 folder_path = 'path/to/your/folder' # 遍历文件夹中的文件 for filename in os.listdir(folder_path): # 构造原始文件路径 old_file_path = os.path.join(folder_path, filename) # 构造新文件名 new_filename = 'geopandas_' + filename # 构造新文件路径 new_file_path = os.path.join(folder_path, new_filename) # 重命名文件 os.rename(old_file_path, new_file_path) print(f'Renamed "{filename}" to "{new_filename}"') 请确保将'path/to/your/folder'替换为你想要重命名文件的实际文件夹路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值