Qt使用xlnt操作Excel(二):导入Excel

这篇讲如何使用xlnt导入excel,虽然xlnt比Qt自带的ActiveQt库操作更简单,但是使用过程中还是存在一些bug。我当前用的分支是8f39375,导入时有个bug会导致有些情况下编写的Excel文件导入时会报错,不知道官方后面会不会修复,现在我们来重现这个bug并做一个临时的处理办法。
继续用上一篇配置好的XlntTest工程,首先设置源代码路径,这样可以单步调试进源代码
Qt使用xlnt操作Excel(二):导入Excel_第1张图片

1.读取excel代码

在工程目录下新建2个xlsx文件,1.xlsx和2.xlsx
Qt使用xlnt操作Excel(二):导入Excel_第2张图片
这里1和2中的内容完全相同,但编辑方式不一样,1.xlsx中B1和B2都用键盘手敲,在2.xlsx中B2从B1直接复制粘贴过来
接下来编写导入的代码,新建一个类叫ExcelTool

//ExcelTool.h
#pragma once

#include 

class ExcelTool
{
public:
	static QList<QStringList> readData();
};
//ExcelTool.cpp
#include "ExcelTool.h"
#include 
#include 
#include 

QList<QStringList> ExcelTool::readData() {
	QList<QStringList> data;
	//打开文件
	QString path = QFileDialog::getOpenFileName(NULL, u8"打开Excel文件", ".", "Excel(*.xls *.xlsx)");
	if (path.isEmpty()) {
		return data;
	}

	xlnt::workbook wb;
	//加载excel
	wb.load(path.toStdString());
	//读取excel内容
	auto ws = wb.active_sheet();
	for (auto row : ws.rows(false)) {
		QStringList rowData;
		for (auto cell : row) {
			//将std::string转化为QString
			rowData << QString::fromStdString(cell.to_string());
		}
		data << rowData;
	}

	return data;
}
//XlntTest.cpp
#include "XlntTest.h"
#include "ExcelTool.h"

XlntTest::XlntTest(QWidget *parent)
	: QWidget(parent)
{
	ui.setupUi(this);

	connect(ui.pushButton, &QPushButton::clicked, [] {
		auto data = ExcelTool::readData();
	});
}

2.复现bug

当读取2.xlsx时没有问题
Qt使用xlnt操作Excel(二):导入Excel_第3张图片
当读取1.xlsx时会在load出报错
Qt使用xlnt操作Excel(二):导入Excel_第4张图片
这里错误指向了源代码的xlsx_consumer.cpp中1934行,抛出了一个invalid_file的错误,原因是sizes don’t match,接下来我们分析为什么会抛出这个异常。

3.源代码分析

打开之前编译的xlnt_all工程,找到detail/serialization/xlsx_consumer.cpp,代码里面很明显,是unique_count != target_.shared_strings().size()不成立,unique_count是1919行读取的一个属性叫uniqueCount的值,那这个属性定义在哪里呢
这里先提一下Excel的组成,Excel的编写是Office Open XML标记语言编写的,其实就是office自己定的一些xml约束,本质还是xml,这是官方的部分说明,也就是说,整个Excel文件就是一堆xml打包而成,接下来我们对1.xlsx和2.xlsx进行拆包,最简单的办法就是将1.xlsx后缀直接改成zip,对1.zip解压就能看到所有的xml,里面的文件结构这里就不多说,关键的文件是xl/sharedStrings.xml这个文件,下面是两个文件中sharedStrings.xml文件内容



<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="4" uniqueCount="4">
    <si>
        <t>张三t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
    <si>
        <t>李四t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
    <si>
        <t>普通人t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
    <si>
        <t>普通人t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
sst>


<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="4" uniqueCount="3">
    <si>
        <t>张三t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
    <si>
        <t>李四t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
    <si>
        <t>普通人t>
        <phoneticPr fontId="1" type="noConversion"/>
    si>
sst>

这里找到了uniqueCount这个属性,1.xlsx值为4,2.xlsx值为3,而且1.xlsx中出现了重复的标签普通人,我们回到异常的那个函数xlsx_consumer::read_shared_string_table很明显是对sharedStrings.xml内容的读取。unique_count找到了,再看target_.shared_strings变量为什么size不是4,代码里面很明显,1925行和1926行就是对标签内容的读取,查看read_rich_text定义就明白是提取标签内容并返回一个rich_text对象,再看target_的add_shared_string函数定义

std::size_t workbook::add_shared_string(const rich_text &shared, bool allow_duplicates)
{
    register_workbook_part(relationship_type::shared_string_table);

    if (!allow_duplicates)
    {
        auto it = d_->shared_strings_ids_.find(shared);

        if (it != d_->shared_strings_ids_.end())
        {
            return it->second;
        }
    }

    auto sz = d_->shared_strings_ids_.size();
    d_->shared_strings_ids_[shared] = sz;
    d_->shared_strings_values_[sz] = shared;

    return sz;
}

可以看到rich_text最终装载到这个shared_strings_values_变量里面,allow_duplicates默认值是false,这里d_->shared_strings_ids_.find(shared)对rich_text进行了检查是否存在,当存在相同rich_text时又对end()进行了检查,查看shared_strings_ids_的定义:

//workbook_impl.hpp
std::unordered_map<rich_text, std::size_t, rich_text_hash> shared_strings_ids_;

也就是这个end()就是检查rich_text的hash值,到这里问题很明显了,在1.xlsx中对两个普通人标签计算的rich_text_hash值相同,单步调试也可以发现在第2个普通人加载的时候提前return了,因此最后shared_strings的大小为3,和unique_count不一样。那给add_shared_string第二个参数设为true让这段代码不检查重复行吗,显然即使不检查std::unordered_map也会对rich_text进行重复检查并且覆盖第一个普通人标签对应的rich_text

4.修改源代码

既然对两个普通人标签计算的hash值相同,那我们让hash值计算不同就行了,找到rich_text的定义,在public下添加一个变量 unique_index 记录行号,修改rich_text.hpp如下

#pragma once

#include 
#include 

#include 
#include 

namespace xlnt {

/// 
/// Encapsulates zero or more formatted text runs where a text run
/// is a string of text with the same defined formatting.
/// 
class XLNT_API rich_text
{
public:
    //...前面代码省略
	uint32_t unique_index = 0;

private:
    /// 
    /// The runs that make up this rich text.
    /// 
    std::vector<rich_text_run> runs_;
};

class XLNT_API rich_text_hash
{
public:
    std::size_t operator()(const rich_text& k) const
    {
        std::size_t res = 0;

        for (auto r : k.runs())
        {
            res ^= std::hash<std::string>()(r.first);
        }
        res ^= std::hash<std::string>()(std::to_string(k.unique_index));

        return res;
    }
};

} // namespace xlnt

修改rich_text.cpp部分函数:

//...
rich_text &rich_text::operator=(const rich_text &rhs)
{
    runs_.clear();
    runs_ = rhs.runs_;
    unique_index = rhs.unique_index;
    return *this;
}
//...
void rich_text::clear()
{
    runs_.clear();
    unique_index = 0;
}
//...
bool rich_text::operator==(const rich_text &rhs) const
{
    if (runs_.size() != rhs.runs_.size()) return false;

    for (std::size_t i = 0; i < runs_.size(); i++)
    {
        if (runs_[i] != rhs.runs_[i]) return false;
    }

    return unique_index == rhs.unique_index;
}
//...

修改函数xlsx_consumer::read_shared_string_table

void xlsx_consumer::read_shared_string_table()
{
    expect_start_element(qn("spreadsheetml", "sst"), xml::content::complex);
    skip_attributes({"count"});

    bool has_unique_count = false;
    std::size_t unique_count = 0;

    if (parser().attribute_present("uniqueCount"))
    {
        has_unique_count = true;
        unique_count = parser().attribute<std::size_t>("uniqueCount");
    }

	uint32_t unique_index = 0;
    while (in_element(qn("spreadsheetml", "sst")))
    {
        expect_start_element(qn("spreadsheetml", "si"), xml::content::complex);
        auto rt = read_rich_text(qn("spreadsheetml", "si"));
		rt.unique_index = unique_index++;
        target_.add_shared_string(rt);
        expect_end_element(qn("spreadsheetml", "si"));
    }

    expect_end_element(qn("spreadsheetml", "sst"));

    if (has_unique_count && unique_count != target_.shared_strings().size())
    {
        throw invalid_file("sizes don't match");
    }
}

重新编译生成,可以看到从1.xlsx和2.xlsx中读出的内容相同

下一篇介绍如何导出excel


Microsoft C++ 异常: xlnt::invalid_cell_reference,位于内存位置 xxx 处

先定位到源代码,cell_reference.cpp的split_reference函数:
Qt使用xlnt操作Excel(二):导入Excel_第5张图片
这里reference_string值“A1048576 C1:C1048576”,查看调用堆栈,上一个函数是cell_reference

cell_reference::cell_reference(const std::string &string)
{
    auto split = split_reference(string, absolute_column_, absolute_row_);

    column(split.first);
    row(split.second);
}

再看上一级调用函数:

range_reference::range_reference(const std::string &range_string)
    : top_left_("A1"), bottom_right_("A1")
{
    auto colon_index = range_string.find(':');

    if (colon_index != std::string::npos)
    {
        top_left_ = cell_reference(range_string.substr(0, colon_index));
        bottom_right_ = cell_reference(range_string.substr(colon_index + 1));
    }
    else
    {
        top_left_ = cell_reference(range_string);
        bottom_right_ = cell_reference(range_string);
    }
}

大概的意思是分割字符串得到行列位置,左上和右下分割靠‘:’分割,这里range_string的值为A1:A1048576 C1:C1048576,跟之前的相比较明显看出这里冒号分割不彻底造成,没有考虑到多个范围的情况,那这个range_string到底从哪里来什么意思呢,再看上级调用,定位到xlsx_consumer::read_worksheet_begin这个函数:

...
if (parser().attribute_present("sqref"))
   {
       const auto sqref = range_reference(parser().attribute("sqref"));
       current_selection.sqref(sqref);
   }
...

到这里可以看到是读取了sqref属性的值,再次刨开1.xslx,终于找到罪魁祸首
Qt使用xlnt操作Excel(二):导入Excel_第6张图片
在sheet1.xml中的一个selection段里面"sqref=“A1:A1048576 C1:C1048576"”,在官方文档终于找到这个selection的解释https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.selection?view=openxml-2.8.1,意思就是说用户在活动单元格中选择的不连续引用范围。没错,我在xlsx中选择了两列后再ctrl+s进行保存就触发了这个bug
Qt使用xlnt操作Excel(二):导入Excel_第7张图片
终究的原因在range_reference::range_reference这个函数处理不当造成的,既然我们是导入excel数据,因此不关心用户的选择,稍作修改只认用户第一个选中的cell,忽略其他选中:

range_reference::range_reference(const std::string &range_string)
    : top_left_("A1"), bottom_right_("A1")
{
    auto colon_index = range_string.find(':');

    if (colon_index != std::string::npos)
    {
        auto space_index = range_string.find(' ');
        top_left_ = cell_reference(range_string.substr(0, colon_index));
        if (space_index == std::string::npos)
        {
            bottom_right_ = cell_reference(range_string.substr(colon_index + 1));
        }
        else
        {
            bottom_right_ = cell_reference(range_string.substr(colon_index + 1, space_index - colon_index - 1));
        }
    }
    else
    {
        top_left_ = cell_reference(range_string);
        bottom_right_ = cell_reference(range_string);
    }
}

Microsoft C++ 异常: xlnt::invalid_sheet_title,位于内存位置 xxx 处

报错的原因很明显,在源代码253行抛出的,原因是tiitle字符串长度太长
Qt使用xlnt操作Excel(二):导入Excel_第8张图片
手动修改excel发现sheet表的title名称的确不能超过31个字符,但这里计算没有考虑到Unicode字符串,导致中文超过10个就会抛出异常,稍作修改就可解决

//需要包含2个头文件
#include 
#include 

//使用wstring_convert计算长度
void worksheet::title(const std::string &title)
{
    // do no work if we don't need to
    if (d_->title_ == title)
    {
        return;
    }
    // excel limits worksheet titles to 31 characters
    std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> cv;
    if (title.empty() || cv.from_bytes(title).length() > 31)
    {
        throw invalid_sheet_title(title);
    }
    //...
}

你可能感兴趣的:(Qt,xlnt,qt,excel)