powershell 批量导出Access图片(OLE对象)到文件

01 前言

这是大半年前的事了,帮一朋友研究如何批量导出Access里面存的图片(OLE对象)。Access没有提供直接导出图片的方法,很郁闷。查过一番资料,都不是很满意,决定自己鼓捣。经过N天的奋战,最后是成了。把思路记录一下,抛砖引玉。

02 正文

(1)准备

准备了一个Prod.accdb文件,只有一张表,其中prodPic存了JPG格式的图片(OLE 对象),结构如下:
powershell 批量导出Access图片(OLE对象)到文件_第1张图片

注意

  • 本例只存了JPG格式的图片,如需其他格式,请自行修改测试

(2)执行

脚本如下:

<#
.DESCRIPTION
   批量导出ACCDB中的JPG图片,保存到指定路径
   测试环境:powershell 5.1 
   2019-08-02

.EXAMPLE
    cmd下执行(powershell.exe 32bit),依次传入3个参数——ACCDB文件位置、JPG输出位置、SQL语句:
    C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -file "D:\XXX\ExpJPGFromAccdb.ps1" "D:\XXX\Prod.accdb" "D:\XXX\exp" "select prodName as name,prodPic as pic from Products"
#>

#获取Access连接【accdb文件】
function Get-AccessConnection-ACCDB
{
	[CmdletBinding()]
	param (
		[string]$filePath,
		[string]$password
	)
	try
	{
		$con = New-Object System.Data.OleDb.OleDbConnection
		$con.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=$filePath;"
		if($password.trim()){
            $con += "Jet OLEDB:Database Password=$password"
        }
        Write-Verbose ("连接字符串:" + $con.ConnectionString)
		$con.Open()
		if ($con.State -eq "Open")
		{
			$con
			Write-Verbose "连接已打开..."
		}
		else
		{
			$null
		}
	}
	catch
	{
		Write-Verbose "获取数据库连接异常..."
        
        if([Environment]::Is64BitProcess){ Write-Verbose "当前环境或为64bit,请切换至32bit环境(32位的powershell)后重试!"}
        Write-Verbose "异常信息:$($Error[0])"
	}
}

#关键在确定开始的地方
function Export-File{
    param([string]$path,
    [byte[]]$data
    )
    try{
        #加入集合
        $list = New-Object System.Collections.ArrayList
        $list.AddRange($data)
        #定位JPG格式文件头
        $index = Get-ExactPosition -conditionCode "FFD8FF" -content $data[0..310]
        Write-Verbose "头部跳过:$index"
        if($index -gt 0){
            1.300 | ForEach-Object{$list.RemoveAt($list.Count-1)} #去掉后面
            1..$index | ForEach-Object{$list.RemoveAt(0)} #去掉前面
            [byte[]]$newData = $list.ToArray() #转为数组
            $stream = New-Object System.IO.MemoryStream($newData,0,$newData.Length) #如果上一步有问题,此步报异常   
            $img = [System.Drawing.Image]::FromStream($stream)
            $img.Save($path)
            return $true
        }
        Write-Host "文件头格式不符,跳过:$path"
        return $false
    }
    catch{
        Write-Host ("保存异常..."+$Error[0].Exception.Message)
        return $false
    }
}

#定位准确的文件头位置
function Get-ExactPosition{
    param(
        [string]$conditionCode, #文件头格式
        [byte[]]$content
    )
    $index = (($content | ForEach-Object{"{0:X2}" -f $_ }) -join "").IndexOf($conditionCode)
    if($index -gt 1){
        $index /= 2
    }
    return $index
}

#主要处理逻辑
function Main-Do{
    param(
        [string]$accfile,
        [string]$savePath,
        [string]$sql
    )
    if(-not (Test-Path $accfile)){
        Write-Host "ACCDB文件不存在,请检查参数!"
        return 
    }
    if(-not (Test-Path $savePath)){
        Write-Host "保存路径不存在,请检查参数!"
        return 
    }
    if([string]::IsNullOrEmpty($sql)){
        Write-Host "请传入有效的SQL!"
        return 
    }
    $count = 0
    $con = Get-AccessConnection-ACCDB -filePath $accfile
    if($con){
        try
        {
            [void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
            $Error.Clear()
            if(-not $savePath.EndsWith("\")){$savePath += "\"}
            $command = New-Object System.Data.OleDb.OleDbCommand($sql, $con)
            $reader = $command.ExecuteReader()
            while($reader.Read()){
                foreach($c in 0..($reader.FieldCount-1)){
                    if("DBTYPE_LONGVARBINARY" -eq $reader.GetDataTypeName($c).ToString() -and $reader[$c].GetType() -ne [System.DBNull]){
                        #拼接文件名
                        if($reader["name"]){
                            $saveImg = $savePath+$reader["name"]+".jpg"
                        }else{
                            $saveImg = $savePath+"Row_"+("{0:d5}" -f ($count+1))+".jpg"
                        }
                        if(Export-File -path $saveImg -data $reader[$c]){
                            #Write-Host "$saveImg 保存成功!"
                            $count++
                        }else{
                            Write-Host "$saveImg 保存失败!"
                        }
                    }else{
                        Write-Verbose "跳过..$c"
                    }
                }
            }
            $reader.Close()
        }
        
        catch [System.Exception]
        {
            Write-Host ("异常.."+$Error[0].Exception.Message)
        }
        finally
        {
            #Write-Host "关闭连接..."
            $con.Close()
        }
        
    }
    else{
        Write-Host "无连接!"
    }
    return $count
}

$count = Main-Do -accfile $args[0] -savePath $args[1] -sql $args[2]
if($count){Write-Host "完成!成功生成${count}个文件"}

其实思路也比较自然:

  1. 读出OLE字段的二进制流
  2. 根据JPG文件头特征定位到JPG文件开始的地方
  3. 截取指定位置的二进制,构造图片流输出即可

脚本使用说明

  • powershell 5.1下测试通过
  • 将脚本另存为.ps1格式,比如此处为:ExpJPGFromAccdb.ps1,使用32位的powershell程序执行。命令格式(参数间要以空格隔开)如下:C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -file “D:\XXX\ExpJPGFromAccdb.ps1” “D:\XXX\Prod.accdb” “D:\XXX\exp” “select prodName as name,prodPic as pic from Products”
  • 如果提示“未在本地计算机上注册“Microsoft.ACE.OLEDB.12.0”提供程序。”,请自行下载安装“Access database engine 2010
  • SQL语句强烈建议只返回两个字段,第一个字段以name为别名,作为导出的文件名;第二个字段为数据库中OLE字段
  • 如果SQL语句中没有返回name字段,默认以Row_行号作为文件名进行保存。这时,如果记录数>99999,为了文件名的统一和美观,可以修改{0:d5}{0:d10}再执行
  • 如果不能执行脚本,提示“无法加载文件 D:\XXX\ExpJPGFromAccdb.ps1,因为在此系统上禁止运行脚本……”。请先修改powershell执行策略(参考此处)

执行过程:
powershell 批量导出Access图片(OLE对象)到文件_第2张图片
导出结果:
powershell 批量导出Access图片(OLE对象)到文件_第3张图片

03 后记

再提醒一点,该脚本目前仅能处理JPG格式图片,其他格式暂无法识别,有条件的可以自己修改。
另外,如果有想把上面的代码改为VBA的,也是可以的,建议引用 Microsoft ActiveX data Objects 2.8 以上。

欢迎留言交流~
------END------

你可能感兴趣的:(powershell)