基于C++的htm静态页面读取脚本

前些天帮hxd写了一个脚本,因为这些天考试什么的一直没空,考完试了抽空来分享一下。
需求: 自动爬取静态htm页面的某些字段值放入excel表中。比如说一个某人期末考试成绩的静态页面,就需要自动把一个学校上万份的成绩直接导入excel表。
分析: 我首先想到的就是使用C++写。第一是因为C++基础相对其它较好,python很久没用了。第二是主要原因,python脚本发给朋友用需要有python环境,而C++生成了exe可以直接发过去使用。
整体思路: 将生成的exe可执行文件与htm页面放在同一个文件路径下,双击运行程序即可。那么首先我们需要获取文件的当前目录。其次获取该目录下的所有文件。然后读取指定文件(这里就是htm文件)的内容,然后使用正则表达式匹配html语言标签的值,再写入excel。

getcwd
首先第一个问题是,如何获取当前程序所在的路径。gcc提供了getcwd直接获取当前路径,需要注意的是,当我想查看其底层源码时,却只能找到下面的代码:

extern inline __attribute__((__always_inline__)) char *
getcwd (char *__buf, size_t __size)
{
  if (__ssp_bos (__buf) != (size_t) -1 && __size > __ssp_bos (__buf))
    __chk_fail ();
  return __getcwd_alias (__buf, __size);
}

这是因为,getcwd是由编译器gcc提供的内置函数,并不是C库函数。
所以,如果我们用llvm等其它编译器进行编译时,这类函数是用不了的。__getcwd_alias的具体函数就看不到,只能溯源到一些宏定义。不过我们还是大概能看懂什么意思,第一次参数是一个指针,然后第二个参数是指向的内存区域的大小,如果判断大小不对就不会报错。所以我实际的用法是:

	char   buffer[MAX_P];
    getcwd(buffer, MAX_P);
extern char *__SSP_REDIRECT (__getcwd_alias,
			     (char *__buf, size_t __size), getcwd);
# define __SSP_REDIRECT(name, proto, alias) \
  name proto __asm__ (__SSP_ASMNAME (#alias))
# define __SSP_ASMNAME(cname)  __SSP_ASMNAME2 (__USER_LABEL_PREFIX__, cname)
# define __SSP_ASMNAME2(prefix, cname) __SSP_ASMNAME3 (prefix) cname
# define __SSP_ASMNAME3(prefix) #prefix

上面的代码并没有看懂,不过感觉其底层实现和汇编有关系。想要具体了解,可能得去看看gcc中patch的实现了,瞄了一眼,不想再看了…贴出链接
传送门
GetAllFiles
然后我们需要获取当前路径下的所有文件

void GetAllFiles( string path, vector<string>& files)  
{  

    long   hFile   =   0;  
    //文件信息  
    struct _finddata_t fileinfo;//用来存储文件信息的结构体  
    string p;  
    if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) !=  -1) 
    {  
        do  
        {   
            if((fileinfo.attrib &  _A_SUBDIR))  //如果查找到的是文件夹
            {  
                if(strcmp(fileinfo.name,".") != 0  &&  strcmp(fileinfo.name,"..") != 0) //不能是当前目录和上一级目录
                {
                    files.push_back(p.assign(path).append("\\").append(fileinfo.name) );
                    GetAllFiles( p.assign(path).append("\\").append(fileinfo.name), files ); //递归搜索
                }
            }  
            else //如果查找到的不是是文件夹 
            {  
                files.push_back(p.assign(fileinfo.name) ); //保存文件名
            } 

        }while(_findnext(hFile, &fileinfo)  == 0);  

        _findclose(hFile); //结束查找
    } 

}

首先需要是使用 _finddata_t 来存储文件信息,分别表示:文件的类型,文件的创建时间,文件最后一次被访问时间,文件最后一次修改时间,文件的大小,文件的名称。

#define _finddata_t _finddata32_t
struct _finddata32_t {
    unsigned attrib;
    __time32_t time_create;
    __time32_t time_access;
    __time32_t time_write;
    _fsize_t size;
    char name[260];
  };

接着使用_findfirst查找文件。_findfirst起始是_findfirst32的宏定义,而_findfirst32函数的第一个参数是const char * 类型的,这也是为什么在传参时要加一个c_str()函数,就是把string类型转换成const char * 类型。该函数返回查找成功的文件句柄,把文件信息存储在结构体中,查找失败返回-1.

// io.h
_CRTIMP intptr_t __cdecl _findfirst32(const char *_Filename,struct _finddata32_t *_FindData);

这里的_Filename路径中可以包括*这种通配符,这样就可以方便地表示某目录下地所有文件。显然,这里可以直接定义文件的类型,不需要后面再加条件进行判断htm文件,但是为了保持函数的统一性,就没有更改,也没有在函数上加参数。另外可惜的是,在io.h定义的内容无法搜索到其源码实现。
同时在io.h中宏定义了一些值来表示文件的类型。

#define _A_NORMAL 0x00  //普通文件
#define _A_RDONLY 0x01 //只读
#define _A_HIDDEN 0x02 //隐藏
#define _A_SYSTEM 0x04 //系统
#define _A_SUBDIR 0x10 //文件夹
#define _A_ARCH 0x20   //存档

所以整体的思路就是,利用_findfirst搜索第一个文件,然后判断是否是文件夹进行递归查询,再用得到的文件句柄使用_findnext继续搜索,最后使用_findclose结束查询。
正则表达式进行匹配
然后就是读取这些文件,并利用正则表达式进行匹配。
正则如何匹配就不讲了,网上一大堆都能搜到。这里给出一个例子。

regex tableReg("<SPAN style=\"COLOR: teal\">姓名 </SPAN>[^ -~]{2,}");
auto ret = regex_search(p,m,tableReg);
if(ret2){
            string oo = m2[0];
            e.WriteCell(ans, 1, oo.substr(19).data());
            cout << oo.substr(19).data() << endl;
        }

我就是用上面这种方式进行匹配特定标签值的。因为我提前查看的所有的htm页面的源码,他们都使用的是同一种格式,所以为了方便我直接计算字符位数截取想要的数值,但是显然这不是一种聪明的方法,有更好的方法欢迎讨论。
写excel
刚开始说实话没想到写excel会这么麻烦,后来在网上看了一圈才明白,excel不像普通文件可以直接读写,网上的大多数方案是导入excel的库,常见的是odbe,ole等方式。但是其实这样做起来是挺麻烦的,短时间内不想深入探究。于是这里想到一个曲线救国的方案,写csv,然后再讲csv文件复制到excel中,反正只是一个自动化实现功能的小脚本。
于是可以直接写一个读写csv的类,这比直接写excel简单多了。

class ExcelProcess
{
public:
    ExcelProcess(){pf=NULL;};
    ~ExcelProcess(void){ if (pf) EndWrite(); };
    
    void WriteCell(ushort row, ushort col, const char *value)
    {
        ushort iLen = (ushort)strlen(value);
        ushort clData[] = { 0x0204, ushort(8 + iLen), row, col, 0, iLen };
        WriteArray(clData, 12);
        WriteArray(value, iLen);
    }

    void WriteCell(ushort row, ushort col, int value)
    {
        ushort clData[] = { 0x027E, 10, row, col, 0 };
        WriteArray(clData, 10);
        int iValue = (value << 2) | 2;
        WriteArray(&iValue, 4);
    }

    void WriteCell(ushort row, ushort col, double value)
    {
        ushort clData[] = { 0x0203, 14, row, col, 0 };
        WriteArray(clData, 10);
        WriteArray(&value, 8);
    }

    void WriteCell(ushort row, ushort col)
    {
        ushort clData[] = { 0x0201, 6, row, col, 0x17 };
        WriteArray(clData, 10);
    }

    bool BeginWrite(const char *fileName)
    {
        pf = fopen(fileName, "wb+");
        if (!pf) return false;
        ushort clBegin[] = { 0x0809, 0x08, 0x0, 0x10, 0x0, 0x0 };
        WriteArray(clBegin, 12);
        return true;
    }

    void EndWrite()
    {
        ushort clEnd[] = { 0x0A, 0x0 };
        WriteArray(clEnd, 4);
        fclose(pf);
        pf = 0;
    }
    bool IsOpen()
    {
        if (pf!=NULL)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    void WriteArray(const void *value, ushort len)
    {
        if (pf)
            fwrite(value, 1, len, pf);
    }
private:
    FILE *pf;
};

这个类可以直接拿去用。
编码问题
当我以为所有的问题都解决时,我使用编译运行,却发现写入的值出现了乱码,于是首先在控制台中对读取的文件内容进行输出,发现在这里就出现乱码。请添加图片描述
为了解决这个问题,首先需要一些编码的基础知识,看了不少文章,这里直接给总结一下:

  1. 先从ascii编码开始,ascii使用7位来表示128个基础字符,如大小写英文字母等等
  2. 中国人通过对ascii编码进行扩展,发明了 GB2312 编码,以表示6000多个常用汉字。
  3. 但是汉字很多,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。所以GB2312是GBK编码的子集。
  4. 中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
  5. 然后iso为了统一提出了UNICODE编码。但是起始准确的说,UNICODE是字符集,能够表示任何一种字符,但只是提供字符的二进制码,没有规定如何编码。
  6. 然后出现了3中对unicode进行编码的方式,分别是utf-8,utf-16le,utf-16be。简单的说就是,unicode编码分为3中,分别是utf-8、utf-16le,utf-16be。
  7. 不得不提的是BOM标志,即byte order mask. 就是放在文件的最开始位置,用来表示编码的顺序,这是一种很容易理解的思维方式。
  8. utf-16 这里的16是指16位,也就是2字节。即每两个字节编码一个字符。(离谱的是就算是英文字符也是要两个字节)。然后utf-16是必有BOM的,分别是LE(0xFF 0xFE) 和 BE(0xFE 0xFF)。这也是为什么utf-16有be和le了。这里的be和le的意思是big endian, low endian,即大段和小端的意思。
  9. utf-8分为有BOM和没有BOM的。utf-8是不定长的,可能是1-3字节。有BOM的utf-8的BOM为3个字节的前缀0xEE BB EF。
  10. 其实正常情况下utf-8编码是不带BOM的,但是是微软的习惯,为了区分uft-8和ascii等编码,一个原因是,英文大小写等基础字母在utf-8和ascii编码中是一样的。同时也是微软的习惯,默认把utf-16le成为unicode编码。

有了以上基础知识,相信一定对编码有了一个宏观的了解,至少不会凌乱了。然后下面回归我们的问题,基于上面的知识,我们可以做出以下分析:

  1. vscode 在写代码的时候默认是utf-8,这在右下角可以看到。
  2. 出现乱码问题的原因是:
    请添加图片描述
    当我试图用vscode打开htm文件时,发现编码是utf-16le。仔细看看上面的乱码,其实就是两个字节编码的原因,导致字符变宽了。
    而代码的编写编码是utf-8,这就导致了正则表达式中的汉字无法匹配的问题,以及控制台终端的编码是gbk的原因,所以显示乱码,以及写入csv的汉字出现乱码。请添加图片描述
  3. 统一编码来解决乱码问题。首先的想法是,因为htm文件打开的默认编码是utf-16le,所以不如把代码编码和控制台编码全部改成utf-16le。vscode里面更改编码很简单,右下角直接改然后重新写代码里面的中文即可。但是在终端窗口中却不好改。网上给出的两种更改方式其实本质都一样,就是利用chcp命令更改,在存在的缺点是,不够自动化。当我把脚本给朋友时,我无法做到直接使用代码更改他的环境上的控制台的编码。所以更好的方式就是,既然windows默认是gbk,那就全部统一为gbk编码。
  4. 如何把文件从utf-16le转换成gbk编码呢?最简单的方式就是用windows记事本打开文件,保存的时候选择编码。
    在这里插入图片描述
    可以看到,这里出现的编码,正好对应上面讲过的编码,utf-8带物料清单就是带BOM头的utf-8.
    那么ANSI是什么编码呢?
    在这里插入图片描述

----引用[ANSI是什么编码](https://blog.csdn.net/imxiangzi/article/details/77370160) 所以其实我们就可以暂且认为,在我们的环境下,ANSI编码就是GBK编码,但是我们不可能一个一个打开文件转换编码吧,我们需要用代码的方式转换文件的编码。

文件编码转换为ANSI编码

/*
 * 将路径fpath所指的文件从各种编码格式,转换为ANSI格式
 *
 * Copyright (c) 2013 赵子清, All rights reserved.
 *
 */
#include <bits/stdc++.h>
#include <windows.h>
#define CODE_FORMAT_ANSI        1
#define CODE_FORMAT_UTF8        2
#define CODE_FORMAT_UNICODE_LE  3
#define CODE_FORMAT_UNICODE_BE  4

typedef int ErrorCode;
#define    ERR_OK                  0
#define    ERR_FILE_OPEN_FAILED    1001

#define SWAP16(x) \
    ((((x) & 0x00ff) << 8) | \
    (((x) & 0xff00) >> 8) )

#define SWAP32(x) \
    ((((x) & 0x000000ff) << 24) | \
    (((x) & 0x0000ff00) <<  8) | \
    (((x) & 0x00ff0000) >>  8) | \
    (((x) & 0xff000000) >> 24) )

#define SAFE_DELETE(x) if((x)!=0) {delete[] (x); (x) = 0; }

ErrorCode  ConvertFormat(const char* fpath)
{
    assert(fpath != 0);

    FILE* fp = ::fopen(fpath, "rb");
    if(fp == NULL)
        return ERR_FILE_OPEN_FAILED;

    int fmtFlag;
    int fmt = CODE_FORMAT_ANSI;
    ::fread(&fmtFlag, sizeof(int), 1, fp);
    ::fclose(fp);

    fmtFlag = SWAP32(fmtFlag);
    if((fmtFlag & 0xffffff00) == 0xefbbbf00)
        fmt = CODE_FORMAT_UTF8;
    else if((fmtFlag & 0xffff0000) == 0xfffe0000)
        fmt = CODE_FORMAT_UNICODE_LE;
    else if((fmtFlag & 0xffff0000) == 0xfeff0000)
        fmt = CODE_FORMAT_UNICODE_BE;


    if(fmt == CODE_FORMAT_ANSI)
        return ERR_OK;

    fp = ::fopen(fpath, "rb");
    char* txt = 0;
    wchar_t* wtxt = 0;
    long flen = 0L;
    ::fseek(fp, 0L, SEEK_END);
    flen = ftell(fp);
    ::rewind(fp);
    if(fmt == CODE_FORMAT_UTF8)
    {
        txt = new char[flen+1];
        ::fread(txt, 1, flen, fp);
        txt[flen] = '\0';
    }
    else if(fmt == CODE_FORMAT_UNICODE_LE || fmt == CODE_FORMAT_UNICODE_BE)
    {
        wtxt = new wchar_t[flen/2 +1];
        ::fread(wtxt, 2, flen/2, fp);
        if(fmt == CODE_FORMAT_UNICODE_BE)
        {
            for(int i=0; i < flen/2; i++)
                wtxt[i] = SWAP16(wtxt[i]);
        }
        wtxt[flen/2] = L'\0';
    }

    ::fclose(fp);

    int nLen;
    wchar_t* pwstr = 0;
    char* pstr = 0;
    switch (fmt)
    {
    case CODE_FORMAT_UTF8:
        nLen = ::MultiByteToWideChar(CP_UTF8, 0, txt+3, -1, NULL, 0);
        pwstr = new wchar_t[nLen+1];
        nLen = ::MultiByteToWideChar(CP_UTF8, 0, txt+3, -1, pwstr, nLen);
        nLen = ::WideCharToMultiByte(CP_ACP, 0, pwstr, -1, NULL, 0, NULL, NULL);
        pstr = new char[nLen];
        ::memset(pstr, 0, nLen);
        nLen = ::WideCharToMultiByte(CP_ACP, 0, pwstr, -1, pstr,
                                nLen, NULL, NULL);
        break;
    case CODE_FORMAT_UNICODE_LE:
    case CODE_FORMAT_UNICODE_BE:
        nLen = ::WideCharToMultiByte(CP_ACP, 0, wtxt+1, -1, NULL, 0, NULL, NULL);
        pstr = new char[nLen];
        ::memset(pstr, 0, nLen);
        nLen = ::WideCharToMultiByte(CP_ACP, 0, wtxt+1, -1, pstr,
                                        nLen, NULL, NULL);
        break;
    default:
        break;
    }

    fp = ::fopen(fpath, "wb");
    ::fwrite(pstr, 1, nLen-1, fp);
    ::fclose(fp);

    SAFE_DELETE(txt);
    SAFE_DELETE(wtxt);
    SAFE_DELETE(pstr);
    SAFE_DELETE(pwstr);

    return ERR_OK;
}

代码引用自赵子清的博客
其本质是使用WideCharToMultiByte函数,在windows.h的头文件中,不具有通用性。
首先,需要知道,::可以表示全局作用域,这是因为有可能定义的局部函数或者变量和全局的名字一样,编译器可能找不到,所以加上::来确定是全局的函数与变量。
其次,读取文件的第一个字节的内容,根据BOM的内容,判断是哪种编码方式。

	#define SWAP32(x) \
    ((((x) & 0x000000ff) << 24) | \
    (((x) & 0x0000ff00) <<  8) | \
    (((x) & 0x00ff0000) >>  8) | \
    (((x) & 0xff000000) >> 24) )
	fmtFlag = SWAP32(fmtFlag);
    if((fmtFlag & 0xffffff00) == 0xefbbbf00)
        fmt = CODE_FORMAT_UTF8;
    else if((fmtFlag & 0xffff0000) == 0xfffe0000)
        fmt = CODE_FORMAT_UNICODE_LE;
    else if((fmtFlag & 0xffff0000) == 0xfeff0000)
        fmt = CODE_FORMAT_UNICODE_BE;

这里SWAP32其实就是交换32位的高低16位,比如0xABCDEF00就会转换为0x00EFCDAB,就是大小端的转换。但是其实这里我认为不需要转换也是可以的,后面作&运算时判断低位即可。
接着,求得file的文件大小。其中fseek定位到seek_end即文件末尾,然后使用ftell求得fp指针所在位置,即flen为文件的大小。

	::fseek(fp, 0L, SEEK_END);
    flen = ftell(fp);

然后再利用rewind讲fp重新指向文件的最开头。

::rewind(fp);

接着就需要读文件了。如果读入的文件是utf-8的形式,直接读,一次一个字节,flen的长度。

if(fmt == CODE_FORMAT_UTF8)
    {
        txt = new char[flen+1];
        ::fread(txt, 1, flen, fp);
        txt[flen] = '\0';
    }

如果是unicode的形式,因为两个字节编码,所以使用wchar_t来存,因为wchar_t其实就是unsigned short 两字节,并且长度减半,接着大小端转换,把所有大段全部转换成小端,这样utf-16be就也转换成了utf-16le,这样就全部当作小端来处理了。

typedef unsigned short wchar_t; //unsigned short两字节

else if(fmt == CODE_FORMAT_UNICODE_LE || fmt == CODE_FORMAT_UNICODE_BE)
    {
        wtxt = new wchar_t[flen/2 +1];
        ::fread(wtxt, 2, flen/2, fp);
        if(fmt == CODE_FORMAT_UNICODE_BE)
        {
            for(int i=0; i < flen/2; i++)
                wtxt[i] = SWAP16(wtxt[i]);
        }
        wtxt[flen/2] = L'\0';
    }

读完文件内容之后,就开始转码了,就是利用WideCharToMultiByte这个由windows提供的api进行转换。utf-8转换成ansi就是先把utf-8转换成utf16,再转成ansi,所以我们直接看utf-16

case CODE_FORMAT_UNICODE_LE:
    case CODE_FORMAT_UNICODE_BE:
        nLen = ::WideCharToMultiByte(CP_ACP, 0, wtxt+1, -1, NULL, 0, NULL, NULL);
        pstr = new char[nLen];
        ::memset(pstr, 0, nLen);
        nLen = ::WideCharToMultiByte(CP_ACP, 0, wtxt+1, -1, pstr,
                                        nLen, NULL, NULL);
        break;

这里简单的讲就是, ::WideCharToMultiByte(CP_ACP的意思是,它的意思是:我已经是utf-16了,我要转化成cp_ acp(ansi)。
同样的,utf-8的步骤中,后两步和这个是一样的,前两步就是先把utf-8转成utf-16 (WideChar 顾名思义就是宽字符,就是两个字节,utf-16两个字节编码)。::MultiByteToWideChar(CP_UTF8 就是意思为: 我现在已经是utf-8了,要转成utf-16.

至此,全部结束,就可以没有乱码地提取出htm文件中相应的属性值了。相关的资源也上传了,可以拿来参考。点击下载

References:

  1. https://www.cnblogs.com/zzqcn/archive/2013/04/25/3043730.html
  2. https://www.cnblogs.com/ranjiewen/p/5770639.html
  3. http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
  4. https://blog.csdn.net/imxiangzi/article/details/77370160
  5. https://blog.csdn.net/yu704645129/article/details/79907625
  6. https://blog.csdn.net/qq1623803207/article/details/89398435
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值