从路径中获取文件名

117

如何从路径中获取文件名?

string filename = "C:\\MyDirectory\\MyFile.bat"
在这个例子中,我应该得到 "MyFile" 这个没有扩展名的文件名。

2
从后往前搜索,直到遇到一个退格键? - Kerrek SB
2
@KerrekSB,你的意思是反斜杠 ;) - Nim
我有一个包含文件路径的std::string,如"c:\MyDirectory\Myfile.pdf",我需要将此文件重命名为myfile_md.pdf,因此我需要从路径中获取文件名。 - nidhal
1
如果您需要处理大量的文件路径,请考虑使用Boost FileSystem http://www.boost.org/doc/libs/release/libs/filesystem/v3/doc/index.htm - edA-qa mort-ora-y
2
@Nim:是啊!我一定走神了... - Kerrek SB
24个回答

97

这个任务相对简单,因为基本文件名只是从文件夹的最后一个分隔符开始的字符串部分:

std::string base_filename = path.substr(path.find_last_of("/\\") + 1)

如果要同时删除扩展名,唯一需要做的是找到最后一个.,并从这里截取一个substr
std::string::size_type const p(base_filename.find_last_of('.'));
std::string file_without_extension = base_filename.substr(0, p);

也许应该检查仅包含扩展名的文件(例如.bashrc...)

如果将其拆分为单独的函数,则可以灵活地重用单个任务:

template<class T>
T base_name(T const & path, T const & delims = "/\\")
{
  return path.substr(path.find_last_of(delims) + 1);
}
template<class T>
T remove_extension(T const & filename)
{
  typename T::size_type const p(filename.find_last_of('.'));
  return p > 0 && p != T::npos ? filename.substr(0, p) : filename;
}

代码采用模板化设计,可以与不同的std::basic_string实例(例如std::string和std::wstring)一起使用。
模板化设计的缺点是需要指定模板参数,如果将const char*传递给函数,则必须这样做。
因此,您可以选择:
A) 只使用std::string而不是对代码进行模板化。
std::string base_name(std::string const & path)
{
  return path.substr(path.find_last_of("/\\") + 1);
}

B) 使用std::string提供包装函数(作为中间变量,可能会被内联/优化掉)

inline std::string string_base_name(std::string const & path)
{
  return base_name(path);
}

C) 在使用const char *调用时指定模板参数。

std::string base = base_name<std::string>("some/path/file.ext");

结果

std::string filepath = "C:\\MyDirectory\\MyFile.bat";
std::cout << remove_extension(base_name(filepath)) << std::endl;

打印

MyFile

在这个使用案例中一切都很好(原始问题已得到解答),但是您的扩展名移除器并不完美 - 如果我们传递类似于“/home/user/my.dir/myfile”这样的内容,它将失败。 - avtomaton
@avtomaton 删除扩展名的函数应该用于文件名而不是路径。(先应用“base_name”)。 - Pixelchemist
我理解了(这就是为什么我写下原始问题已经得到解答,在这种情况下一切都很好)。只是想为那些尝试使用这些片段的人指出这个问题。 - avtomaton
非常好的解释,它增强了对问题的结构理解。谢谢。 - hell_ical_vortex
反斜杠实际上是Unix系统中有效的文件名字符。你的解决方案对Unix路径/home/user/my\file.txt给出了错误的结果。 - eyelash

70

一种可能的解决方案:

string filename = "C:\\MyDirectory\\MyFile.bat";

// Remove directory if present.
// Do this before extension removal incase directory has a period character.
const size_t last_slash_idx = filename.find_last_of("\\/");
if (std::string::npos != last_slash_idx)
{
    filename.erase(0, last_slash_idx + 1);
}

// Remove extension if present.
const size_t period_idx = filename.rfind('.');
if (std::string::npos != period_idx)
{
    filename.erase(period_idx);
}

最简单的方式永远是最好的! - Jean-François Fabre

67

C++17中最简单的方法如下:

使用#include <filesystem>,并使用filename()获取带扩展名的文件名和使用stem()获取不带扩展名的文件名。

#include <iostream>
#include <string>
#include <filesystem>
namespace fs = std::filesystem;

int main()
{
  std::string filename = "C:\\MyDirectory\\MyFile.bat";

  std::cout << fs::path(filename).filename() << '\n'
    << fs::path(filename).stem() << '\n'
    << fs::path("/foo/bar.txt").filename() << '\n'
    << fs::path("/foo/bar.txt").stem() << '\n'
    << fs::path("/foo/.bar").filename() << '\n'
    << fs::path("/foo/bar/").filename() << '\n'
    << fs::path("/foo/.").filename() << '\n'
    << fs::path("/foo/..").filename() << '\n'
    << fs::path(".").filename() << '\n'
    << fs::path("..").filename() << '\n'
    << fs::path("/").filename() << '\n';
}

可以使用 g++ -std=c++17 main.cpp -lstdc++fs 进行编译,并输出:

"MyFile.bat"
"MyFile"
"bar.txt"
"bar"
".bar"
""
"."
".."
"."
".."
"/"

参考: cppreference


它不再是“实验性的”了。 - Vit
命名:你知道为什么选择“stem”而不是“basename”吗? - pmor

40
最简单的解决方案是使用类似于boost::filesystem的东西。如果出于某些原因这不可行... 在正确处理这个问题时需要一些依赖于系统的代码:在Windows下,'\\''/'都可以作为路径分隔符;在Unix下,只有'/'起作用,在其他系统下,谁知道呢。显而易见的解决方案可能是这样的:
std::string
basename( std::string const& pathname )
{
    return std::string( 
        std::find_if( pathname.rbegin(), pathname.rend(),
                      MatchPathSeparator() ).base(),
        pathname.end() );
}

当定义头文件系统有关时,MatchPathSeparator 可以被定义为以下之一:

struct MatchPathSeparator
{
    bool operator()( char ch ) const
    {
        return ch == '/';
    }
};

对于Unix系统:

struct MatchPathSeparator
{
    bool operator()( char ch ) const
    {
        return ch == '\\' || ch == '/';
    }
};

对于Windows系统(或其他未知系统),需要做一些不同的事情。

编辑:我忽略了他还想抑制扩展名的事实。为此,需要更多相同的操作:

std::string
removeExtension( std::string const& filename )
{
    std::string::const_reverse_iterator
                        pivot
            = std::find( filename.rbegin(), filename.rend(), '.' );
    return pivot == filename.rend()
        ? filename
        : std::string( filename.begin(), pivot.base() - 1 );
}

这段代码稍微有些复杂,因为在这种情况下,反向迭代器的基础位于我们想要切割的位置错误的一侧。 (请记住,反向迭代器的基础是指向迭代器所指字符后面的一个位置。)即使如此,这还是有点可疑的:例如,我不喜欢它可能返回空字符串的事实。 (如果唯一的'.'是文件名的第一个字符,我认为您应该返回完整的文件名。这需要一点额外的代码来捕获特殊情况。)


10
可以考虑使用 string::find_last_of 而不是使用反向迭代器进行操作吗? - Luc Touraille
@LucTouraille 为什么要学习两种做事的方式,当一种就可以呢?除了 string 之外,您需要反向迭代器来处理任何容器,因此您必须学习它们。而且一旦学会了它们,就没有必要费心学习所有臃肿的 std::string 接口了。 - James Kanze
注意:<filesystem>头文件随Visual Studio 2015及以上版本一起发布,因此您无需添加依赖项boost即可使用它。 - IInspectable

32

_splitpath应该能够满足您的需求。当然,您也可以手动完成,但_splitpath也处理所有特殊情况。

编辑:

正如BillHoag所提到的,建议在可用时使用更安全的_splitpath_s版本。

或者,如果您想要可移植性,可以像这样做:

std::vector<std::string> splitpath(
  const std::string& str
  , const std::set<char> delimiters)
{
  std::vector<std::string> result;

  char const* pch = str.c_str();
  char const* start = pch;
  for(; *pch; ++pch)
  {
    if (delimiters.find(*pch) != delimiters.end())
    {
      if (start != pch)
      {
        std::string str(start, pch);
        result.push_back(str);
      }
      else
      {
        result.push_back("");
      }
      start = pch + 1;
    }
  }
  result.push_back(start);

  return result;
}

...
std::set<char> delims{'\\'};

std::vector<std::string> path = splitpath("C:\\MyDirectory\\MyFile.bat", delims);
cout << path.back() << endl;

2
我的机器上没有任何包含_splitpath的内容。 - James Kanze
10
我有Visual Studio、g++和Sun CC。既然有完全可靠的便携式解决方案,为什么还要使用非标准的东西呢? - James Kanze
2
@James,链接页面显示它在<stdlib.h>中。至于可移植性,也许你可以列举一些“完全好的可移植解决方案”的例子? - Synetech
2
@Synetech,链接页面描述的是微软扩展,而不是<stdlib.h>。最明显的可移植解决方案是boost::filesystem - James Kanze
3
@James,你的VS副本的stdlib.h中没有_splitpath函数吗?那么你可能需要进行VS的修复安装。 - Synetech
显示剩余5条评论

17

如果您能使用boost库,

#include <boost/filesystem.hpp>
boost::filesystem::path p("C:\\MyDirectory\\MyFile.bat");
string basename = p.filename().string();
//or 
//string basename = boost::filesystem::path("C:\\MyDirectory\\MyFile.bat").filename().string();

就这些了。

我建议你使用Boost库。在使用C++时,Boost库可以为你带来很多便利。它支持几乎所有平台。如果你使用Ubuntu,你只需要一行命令sudo apt-get install libboost-all-dev就可以安装Boost库(参考:如何在Ubuntu上安装Boost)。


16

您还可以使用 shell Path APIs PathFindFileName、PathRemoveExtension。 对于这个特定问题来说,可能比 _splitpath 差,但是这些 API 非常适用于各种路径解析工作,并且考虑了 UNC 路径、正斜杠和其他奇怪的东西。

wstring filename = L"C:\\MyDirectory\\MyFile.bat";
wchar_t* filepart = PathFindFileName(filename.c_str());
PathRemoveExtension(filepart); 

http://msdn.microsoft.com/en-us/library/windows/desktop/bb773589(v=vs.85).aspx

这种做法的缺点是你需要链接shlwapi.lib库,但我并不确定为什么这是个缺点。

从路径中获取文件名的首选解决方案。 - Andreas

14

功能:

#include <string>

std::string
basename(const std::string &filename)
{
    if (filename.empty()) {
        return {};
    }

    auto len = filename.length();
    auto index = filename.find_last_of("/\\");

    if (index == std::string::npos) {
        return filename;
    }

    if (index + 1 >= len) {

        len--;
        index = filename.substr(0, len).find_last_of("/\\");

        if (len == 0) {
            return filename;
        }

        if (index == 0) {
            return filename.substr(1, len - 1);
        }

        if (index == std::string::npos) {
            return filename.substr(0, len);
        }

        return filename.substr(index + 1, len - index - 1);
    }

    return filename.substr(index + 1, len - index);
}

测试:

#define CATCH_CONFIG_MAIN
#include <catch/catch.hpp>

TEST_CASE("basename")
{
    CHECK(basename("") == "");
    CHECK(basename("no_path") == "no_path");
    CHECK(basename("with.ext") == "with.ext");
    CHECK(basename("/no_filename/") == "no_filename");
    CHECK(basename("no_filename/") == "no_filename");
    CHECK(basename("/no/filename/") == "filename");
    CHECK(basename("/absolute/file.ext") == "file.ext");
    CHECK(basename("../relative/file.ext") == "file.ext");
    CHECK(basename("/") == "/");
    CHECK(basename("c:\\windows\\path.ext") == "path.ext");
    CHECK(basename("c:\\windows\\no_filename\\") == "no_filename");
}

非常好!谢谢! - x4444

9

From C++ Docs - string::find_last_of

#include <iostream>       // std::cout
#include <string>         // std::string

void SplitFilename (const std::string& str) {
  std::cout << "Splitting: " << str << '\n';
  unsigned found = str.find_last_of("/\\");
  std::cout << " path: " << str.substr(0,found) << '\n';
  std::cout << " file: " << str.substr(found+1) << '\n';
}

int main () {
  std::string str1 ("/usr/bin/man");
  std::string str2 ("c:\\windows\\winhelp.exe");

  SplitFilename (str1);
  SplitFilename (str2);

  return 0;
}

输出:

Splitting: /usr/bin/man
 path: /usr/bin
 file: man
Splitting: c:\windows\winhelp.exe
 path: c:\windows
 file: winhelp.exe

不要忘记(并处理)如果未找到任何内容,find_last_of将返回string :: npos - congusbongus
@congusbongus 是的,但如果只是文件名(没有路径),那么分割文件路径就没有意义了 :) - jave.web
@jave.web 这确实有意义,而且必须处理返回值为'string::npos'的情况。实现一个函数应该能够处理不同的输入,包括“仅文件名”。否则,在实际实现中出现错误时,它将是无用的。 - winux
@winux 这已经考虑了有效路径... 如果您不信任输入,当然应该先验证路径。 - jave.web
无论如何,由于这种方式以及string :: substr的实现方式,不需要检查字符串::npos。a) string::npos 作为“长度”被传递=> substr 具有读取直到结束的记录行为。b) 给出 “string::npos +1” 而没有长度,string::npos 记录为 -1,所以计算结果为0=> 字符串的开始和 lengths'默认值为 substr 的 npos=> 也适用于“只有文件名”。http://www.cplusplus.com/reference/string/string/substr/ http://www.cplusplus.com/reference/string/string/npos/ - jave.web

7

这是一个受James Kanze版本启发的C++11 variant,支持统一初始化和匿名内联lambda函数。

std::string basename(const std::string& pathname)
{
    return {std::find_if(pathname.rbegin(), pathname.rend(),
                         [](char c) { return c == '/'; }).base(),
            pathname.end()};
}

然而,它并不会删除文件扩展名。


简短而简洁,尽管它仅适用于非Windows路径。 - Volomike
你可以随时将lambda返回更改为return c == '/' || c == '\\';,以使其在Windows上正常工作。 - rejkowic
为了处理路径,例如“”,“///”和“dir1/dir2/”,请在上面的返回语句之前添加以下代码(参见POSIX basename()): if (pathname.size() == 0) return "."; auto iter = pathname.rbegin(); auto rend = pathname.rend(); while (iter != rend && *iter == '/') ++iter; if (iter == rend) /* pathname has only path separators */ return "/"; pathname = std::string(pathname.begin(), iter.base()); - Gidfiddle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接