PHP 第6章 文件操作 PHP 第6章 文件操作

2019-08-25

一、同时写入一个文件

1.1、方案一

function writeData($filepath, $data) 

    $fp = fopen($filepath,'a');  
    do{ 
        usleep(100); 
    }while (!flock($fp, LOCK_EX)); 
    
    $res = fwrite($fp, $data."\n"); 
    flock($fp, LOCK_UN); 
    fclose($fp);  
    return $res; 
}

1.2、方案二

function writeData($path, $mode,$data,$max_retries = 10)  
{  
    $fp = fopen($path, $mode);   
    $retries = 0;   
    do{  
       if ($retries > 0)   
       {  
            usleep(rand(1, 10000));  
       }  
       $retries += 1;
    }while (!flock($fp, LOCK_EX) and $retries<= $max_retries);   
    //判断是否等于最大重试次数,是则返回false
    if ($retries == $max_retries)   
    {  
       return false;  
    }  
    fwrite($fp, "$data
");  
    flock($fp, LOCK_UN);  
    fclose($fp);   
    return true;   
}

1.3、方案三

function write_file($filename, $content)
{
    $lock = $filename . '.lck';
    $write_length = 0;
    while(true) {
        if( file_exists($lock) ) {
            usleep(100);
        } else {
            touch($lock);
            $write_length = file_put_contents($filename, $content, FILE_APPEND);
            break;
        }
    }
    if( file_exists($lock) ) {
        unlink($lock);
    }
    return $write_length;
}

1.4、flock

(PHP 4, PHP 5, PHP 7)

flock — 轻便的咨询文件锁定

bool flock ( resource $handle , int $operation [, int &$wouldblock ] )
  • flock() 允许执行一个简单的可以在任何平台中使用的读取/写入模型(包括大部分的 Unix 派生版和甚至是 Windows)。

  • 在 PHP 5.3.2版本之前,锁也会被 fclose() 释放(在脚本结束后会自动调用)。

  • PHP 支持以咨询方式(也就是说所有访问程序必须使用同一方式锁定, 否则它不会工作)锁定全部文件的一种轻便方法。 默认情况下,这个函数会阻塞到获取锁;这可以通过下面文档中 LOCK_NB 选项来控制(在非 Windows 平台上)。

参数

handle

文件系统指针,是典型地由 fopen() 创建的 resource(资源)。

operation

operation 可以是以下值之一:

  • LOCK_SH 取得共享锁定(读取的程序)。

  • LOCK_EX 取得独占锁定(写入的程序。

  • LOCK_UN 释放锁定(无论共享或独占)。

如果不希望 flock() 在锁定时堵塞,则是 LOCK_NB(Windows 上还不支持)。

wouldblock

如果锁定会堵塞的话(EWOULDBLOCK 错误码情况下),可选的第三个参数会被设置为 TRUE。(Windows 上不支持)

返回值

成功时返回 TRUE, 或者在失败时返回 FALSE。

Example #1 flock() 例子

$fp = fopen("/tmp/lock.txt", "r+");
if (flock($fp, LOCK_EX)) {  // 进行排它型锁定
    ftruncate($fp, 0);      // truncate file
    fwrite($fp, "Write something here\n");
    fflush($fp);            // flush output before releasing the lock
    flock($fp, LOCK_UN);    // 释放锁定
} else {
    echo "Couldn't get the lock!";
}
fclose($fp);

Example #2 flock() 使用 LOCK_NB 选项

$fp = fopen('/tmp/lock.txt', 'r+');
/* Activate the LOCK_NB option on an LOCK_EX operation */
if(!flock($fp, LOCK_EX | LOCK_NB)) {
    echo 'Unable to obtain lock';
    exit(-1);
}
/* ... */
fclose($fp);

Note:

  • 由于 flock() 需要一个文件指针, 因此可能不得不用一个特殊的锁定文件来保护打算通过写模式打开的文件的访问(在 fopen() 函数中加入 "w" 或 "w+")。

Warning:

  • 在部分操作系统中 flock() 以进程级实现。当用一个多线程服务器 API(比如 ISAPI)时,可能不可以依靠 flock() 来保护文件,因为运行于同一服务器实例中其它并行线程的 PHP 脚本可以对该文件进行处理。

  • flock() 不支持旧的文件系统,如 FAT 以及它的派生系统。因此,此环境下总是返回 FALSE(尤其是对 Windows 98 用户来说)。

二、断点续传

2.1、php 文件下载

①、文件的基本知识

A 文件的类型:文本文件(.txt)、二进制文件(图片、音频、视频)

B 文件前面有一个 <文件头>,但是是对我们隐藏的

C $fp = fopen("luluqi.txt","rt");

$fp 相当于指针,可比作你翻阅书籍时,用手(指针)指着文字

$fp ——> 指向 luluqi.txt 的 <文件头>,一步步读取文件

②、文件下载原理

③、文件下载代码

//file_name文件名,file_sub_die文件的子路径
function downFile($file_name,$file_sub_dir)
{
     //iconv() 可将字符串从UI中编码转为另一种编码
     //需要转换的原因:php文件函数太古老,需要对中文进行转码
     $file_name = iconv("utf-8","gb2312",$file_name);
     //$_SERVER["DOCUMENT_ROOT"]获取apache的主目录
     $file_path = $_SERVER["DOCUMENT_ROOT"].$file_sub_dir.$file_name;
     file_exists($file_path) or die("文件不存在");
     $fp = fopen($file_path,"r");
     $file_size = filesize($file_path); //filesize获取下载文件的大小
     header("Content-type:application/octet-stream");//返回的是文件
     header("Accept-Ranges:bytes");//按字节大小返回
     header("Accept-length:$file_size");//返回文件大小
     //这里是客户端的弹出对话框,对应的文件名
     header("Content-Disposition:attachment;filename=".$file_name);
     $buffer = 1024;
     $file_count = 0; //为了下载安全,做一个文件字节读取器
     //feof()测试文件指针是否到了文件结束位置
     while (!feof($fp) && ($file_size-$file_count > 0)) {
         //fread($fp,$buffer)从文件指针fp读取最多buffer字节
         $file_data = fread($fp,$buffer);
         $file_count += $buffer;
         echo $file_data;
     }
     fclose($fp);
}
downFile("luluqi.txt","/");

2.2、断点下载原理

①、http协议从1.1开始支持静态获取文件的部分内容,为多线程下载和断点续传提供了技术支持。

它通过在Header里两个参数实现的,客户端发请求时对应的是Accept-Range ,服务器端响应时对应的是 Content-Range

②、具体的请求和响应格式是这样

  • Range:(unit=first byte pos)-[last byte pos] 用于请求头中,指定第一个字节的位置和最后一个字节的位置。

  • Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth] 用于响应头,指定整个实体中的一部分的插入位置,他也指示了整个实体的长度。在服务器向客户返回一个部分响应,它必须描述响应覆盖的范围和整个实体长度。

  • 请求下载整个文件: 

GET /test.rar HTTP/1.1
Connection: close
Host: 116.1.219.219
Range: bytes=0-801 //一般请求下载整个文件是bytes=0- 或不用这个头
Range头域:Range头域可以请求实体的一个或者多个子范围。
例如,  表示头500个字节:bytes=0-499   
表示第二个500字节:bytes=500-999
表示最后500个字节:bytes=-500
表示500字节以后的范围:bytes=500-
第一个和最后一个字节:bytes=0-0,-1
同时指定几个范围:bytes=500-600,601-999
但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200 (OK)。
  • 一般正常回应:

HTTP/1.1 200 OK
Content-Length: 801      
Content-Type: application/octet-stream
Content-Range: bytes 0-800/801 //801:文件总大小

③、问题及应对

有些虚拟机配置时它是不支持断点传输的,下载时不显示总大小和进度条,只能看到下载了多少,甚至是一直处于 0 ,直到下载完成,这在下载文件时很不爽;此外,服务器对 apk 文件类型没有指明是 Content-type: application/octet-stream ,造成火狐浏览器对安卓应用 apk 以文本方式输入,这是不是更不方便。

基于此,有必要通过 PHP 对这种特殊的情况作处理,方法是浏览器给这个 PHP 脚本发送 GET 请求下载文件,PHP 脚本给浏览器响应的 header 中包含 Content-Type: application/octet-stream 和 Content-Range: bytes 0-800/801 即可。同时须用 flush() 函数刷新浏览器的cache ,就能看到下载了多少,否则下载大小一直处于 0

2.3、断点下载代码

/**
 * PHP-HTTP断点续传实现
 * @param string $path: 文件所在路径
 * @param string $file: 文件名
 * @return void
 */
function download($path,$file) {
$real = $path.'/'.$file;
if(!file_exists($real)) {
return false;
}
$size = filesize($real);
$size2 = $size-1;
$range = 0;
if(isset($_SERVER['HTTP_RANGE'])) {   //http_range表示请求一个实体/文件的一个部分,用这个实现多线程下载和断点续传!
header('HTTP /1.1 206 Partial Content');
$range = str_replace('=','-',$_SERVER['HTTP_RANGE']);
$range = explode('-',$range);
$range = trim($range[1]);
header('Content-Length:'.$size);
header('Content-Range: bytes '.$range.'-'.$size2.'/'.$size);
} else {
header('Content-Length:'.$size);
header('Content-Range: bytes 0-'.$size2.'/'.$size);
}
header('Accenpt-Ranges: bytes');
header('Content-Type: application/octet-stream');
header("Cache-control: public");
header("Pragma: public");
//解决在IE中下载时中文乱码问题
$ua = $_SERVER['HTTP_USER_AGENT'];
if(preg_match('/MSIE/',$ua)) {    //表示正在使用 Internet Explorer。
$ie_filename = str_replace('+','%20',urlencode($file));
header('Content-Disposition:attachment; filename='.$ie_filename);
} else {
header('Content-Disposition:attachment; filename='.$file);
}
$fp = fopen($real,'rb+');
fseek($fp,$range);                //fseek:在打开的文件中定位,该函数把文件指针从当前位置向前或向后移动到新的位置,新位置从文件头开始以字节数度量。成功则返回 0;否则返回 -1。注意,移动到 EOF 之后的位置不会产生错误。
while(!feof($fp)) {               //feof:检测是否已到达文件末尾 (eof)
set_time_limit(0);              //注释①
print(fread($fp,1024));         //读取文件(可安全用于二进制文件,第二个参数:规定要读取的最大字节数)
ob_flush();                     //刷新PHP自身的缓冲区
flush();                       //刷新缓冲区的内容(严格来讲, 这个只有在PHP做为apache的Module(handler或者filter)安装的时候, 才有实际作用. 它是刷新WebServer(可以认为特指apache)的缓冲区.)
}
fclose($fp);
}

2.4、防盗链

①、盗链

某内容不在自己的服务器,而通用某种手段获取资源供自己用户使用

②、案例分析

if(isset($_SERVER["HTTP_REFERER"])){
    //$_SERVER["HTTP_REFERER"]  引导用户代理到当前页的前一页地址
    if(strpos($_SERVER["HTTP_REFERER"],"http://localhost/http") === 0){
      echo "luluqi的账号信息";
  }else{
      //跳转警告页面,即在目录外处理想访问本页面的都不可以
      header("Location: error.php");
  }
}else{
    header("Location: error.php");
}
error.php代码:你是盗链用户
http://localhost/http下的a.html:      跳转到test.php运行结果:luluqi的账号信息
不在http://localhost/http下的a.html: 跳转到test.php运行结果:你是盗链用户

三、异步带进度条

3.1、注意事项

①、检查 php.ini 中的三个配置项:memory_limit、upload_max_filesize、post_max_size

②、php5.4 新增的一个功能 使用 $_SESSION 获取上传进度。但是获得的 $_SESSION 总为空。实际上这个功能可能无法使用,因为文档中的注释有这么一段。

Note, this feature doesn't work, when your webserver is runnig PHP via FastCGI. 
There will be no progress informations in the session array.

如果php是通过FastCGI模式运行的web服务器之后,这个特性无法使用。主要是因为在php获取到客户端的输入之前,上传文件已经在web服务器上完成了,因此总是100%。

③、该实例是使用 jquery.form.js 实现的

3.2、演示

3.3、代码下载

四、压缩解压文件

4.1、压缩文件

①、ZipArchive::open

ZipArchive::open ( string $filename [, int $flags ] )

第2个参数讲解

ZIPARCHIVE::OVERWRITE    总是创建一个新的文件,如果指定的zip文件存在,则会覆盖掉
ZIPARCHIVE::CREATE     如果指定的zip文件不存在,则新建一个
ZIPARCHIVE::EXCL      如果指定的zip文件存在,则会报错
ZIPARCHIVE::CHECKCONS

返回值:如果返回值等于下面的属性,表示对应的错误 或者 返回TRUE

$res == ZipArchive::ER_EXISTS    File already exists.(文件已经存在)
$res == ZipArchive::ER_INCONS    Zip archive inconsistent.(压缩文件不一致)
$res == ZipArchive::ER_INVAL     Invalid argument.(无效的参数)
$res == ZipArchive::ER_MEMORY    Malloc failure.(内存错误?这个不确定)
$res == ZipArchive::ER_NOENT     No such file.(没有这样的文件)
$res == ZipArchive::ER_NOZIP     Not a zip archive.(没有一个压缩文件)
$res == ZipArchive::ER_OPEN     Can't open file.(不能打开文件)
$res == ZipArchive::ER_READ     Read error.(读取错误)
$res == ZipArchive::ER_SEEK     Seek error.(查找错误)

②、压缩文件

$fileList = ["test.txt","test.html"];
$filename = "test.zip";
$zip = new ZipArchive();
$zip->open($filename,ZipArchive::CREATE);   //打开压缩包
foreach($fileList as $file){
    $zip->addFile($file,basename($file));   //向压缩包中添加文件
}
$zip->close();  //关闭压缩包

③、压缩文件夹

function addFileToZip($path,$zip){
    $handler=opendir($path); //打开当前文件夹由$path指定。
    while(($filename=readdir($handler))!==false){
        if($filename != "." && $filename != ".."){//文件夹文件名字为'.'和‘..’,不要对他们进行操作
            if(is_dir($path."/".$filename)){// 如果读取的某个对象是文件夹,则递归
                addFileToZip($path."/".$filename, $zip);
            }else{ //将文件加入zip对象
                $zip->addFile($path."/".$filename);
            }
        }
    }
    @closedir($path);
}
$zip=new ZipArchive();
if($zip->open('images.zip', ZipArchive::CREATE)=== TRUE){
    addFileToZip('test/', $zip); //调用方法,对要打包的根目录进行操作,并将ZipArchive的对象传递给方法
    $zip->close(); //关闭处理的zip文件
}

4.2、解压压缩包

①、方法一

$zip = new ZipArchive;
$res = $zip->open('images.zip');
if ($res === TRUE) {
    echo 'ok';
    //解压缩到images文件夹
    $zip->extractTo('images');
    $zip->close();
} else {
    echo 'failed, code:' . $res;
}

②、方法二

function get_zip_originalsize($filename, $path) {
//先判断待解压的文件是否存在
if(!file_exists($filename)){
die("文件 $filename 不存在!");
}
$starttime = explode(' ',microtime()); //解压开始的时间
//将文件名和路径转成windows系统默认的gb2312编码,否则将会读取不到
$filename = iconv("utf-8","gb2312",$filename);
$path = iconv("utf-8","gb2312",$path);
//打开压缩包
$resource = zip_open($filename);
$i = 1;
//遍历读取压缩包里面的一个个文件
while ($dir_resource = zip_read($resource)) {
//如果能打开则继续
if (zip_entry_open($resource,$dir_resource)) {
//获取当前项目的名称,即压缩包里面当前对应的文件名
$file_name = $path.zip_entry_name($dir_resource);
//以最后一个“/”分割,再用字符串截取出路径部分
$file_path = substr($file_name,0,strrpos($file_name, "/"));
//如果路径不存在,则创建一个目录,true表示可以创建多级目录
if(!is_dir($file_path)){
mkdir($file_path,0777,true);
}
//如果不是目录,则写入文件
if(!is_dir($file_name)){
//读取这个文件
$file_size = zip_entry_filesize($dir_resource);
//最大读取6M,如果文件过大,跳过解压,继续下一个
if($file_size<(1024*1024*6)){
$file_content = zip_entry_read($dir_resource,$file_size);
file_put_contents($file_name,$file_content);
}else{
echo " ".$i++." 此文件已被跳过,原因:文件过大, -> ".iconv("gb2312","utf-8",$file_name)." ";
}
}
//关闭当前
zip_entry_close($dir_resource);
}
}
//关闭压缩包
zip_close($resource);
$endtime = explode(' ',microtime()); //解压结束的时间
$thistime = $endtime[0]+$endtime[1]-($starttime[0]+$starttime[1]);
$thistime = round($thistime,3); //保留3为小数
echo "解压完毕!,本次解压花费:$thistime 秒。";
}
$size = get_zip_originalsize('images.zip','temp/');

4.3、其它函数

①、ZipArchive::getNameIndex ( int $index [, int $flags ] )

根据压缩文件内的列表索引,返回被压缩文件的名称 

$zip = new ZipArchive();
$res = $zip->open('test.zip');
if ($res === TRUE) {
var_dump($zip->getNameIndex(0));
} else {
echo 'failed, code:' . $res;
}
$zip->close();

②、 ZipArchive::getStream ( string $name )

根据压缩内的文件名称,获取该文件的文本流 

$zip = new ZipArchive(); 
$res = $zip->open('test.zip'); 
if ($res === TRUE) { 
$stream = $zip->getStream('hello.txt'); 
} else { 
echo 'failed, code:' . $res; 

$zip->close(); 
$str = stream_get_contents($stream); //这里注意获取到的文本编码 
var_dump($str);

③、ZipArchive::renameIndex ( int $index , string $newname ) 

根据压缩文件内的索引(从0开始)修改压缩文件内的文件名 

$zip = new ZipArchive;
$res = $zip->open('test.zip');
if ($res === TRUE) {
    //把压缩文件内第一个文件修改成newname.txt
    $zip->renameIndex(0,'newname.txt');
    $zip->close();
} else {
    echo 'failed, code:' . $res;
}

④、ZipArchive::renameName 

根据压缩文件内的文件名,修改压缩文件内的文件名 

$zip = new ZipArchive;
$res = $zip->open('test.zip');
if ($res === TRUE) {
    //把压缩文件内的word.txt修改成newword.txt
    $zip->renameName('word.txt','newword.txt');
    $zip->close();
} else {
    echo 'failed, code:' . $res;
}

⑤、ZipArchive::setArchiveComment

设置或修改压缩文件的注释(zip的文件注释) 

$zip = new ZipArchive;
$res = $zip->open('test.zip', ZipArchive::CREATE);
if ($res === TRUE) {
    $zip->setArchiveComment('new archive comment');
    $zip->close();
    echo 'ok';
} else {
    echo 'failed';
}

五、文件哈希值

①、什么是哈希值?

哈希值(hash values)是使用哈希函数(hash function)计算得到的值。哈希函数是是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。就是根据数据内容计算得到相应的"摘要",根据这个摘要可以区分该数据与其他数据。

②、如何计算文件的哈希值?

在linux下可以使用以下命令计算

md5sum test.txt
sha1sum test.txt     
sha224sum test.txt
sha256sum test.txt
sha384sum test.txt
sha512sum test.txt

在PHP下可以使用以下命令计算

sha1_file("test.txt");
md5_file("test.txt");

六、文件扩展名

要求:dir/upload.image.jpg,找出 .jpg 或者 jpg , 必须使用 PHP 自带的处理函数进行处理,方法不能明显重复,可以封装成函数,比如 get_ext1($file_name), get_ext2($file_name)

方法一:strtok

该函数会保持它自己的内部指针在字符串中的位置,如果想重置指针,可以将该字符串传给这个函数。所以当第二次调用strtok()函数时,如果对上一次的已分割的字符串进行分割,第1个参数可以省略。

$string= 'dir/upload.image.jpg'; 
$tok = strtok($string, '.'); //使用strtok将字符串分割成一个个令牌 
while ($tok) 

  $arr[]= $tok;  
  $tok = strtok('.'); //
                        //  
   

$count= count($arr); 
$i= $count-1; 
$file_type= $arr[$i];

方法二:explode,使用explode()函数分割字符串,返回值是一个数组

$string= 'dir/upload.image.jpg'; 
$arr= explode('.', $string); // 
$count= count($arr); 
$count-=1; 

$file_type= $arr[$count];//利用数字索引 
$file_type = array_pop($arr);//将数组最后一个单元弹出(出栈),用一个变量接住

方法三:strrpos,得到指定分割符在字符串的最后一次出现的位置

$string= 'dir/upload.image.jpg'; 
$i= strrpos($string, '.');   //得到指定分割符在字符串的最后一次出现的位置 
$file_type= substr($string, $i);//截取字符串 ?>

方法四:strrchr() 函数(在php中)查找字符在指定字符串中从右面开始的第一次出现的位置,如果成功,返回该字符以及其后面的字符,如果失败,则返回 NULL。与之相对应的是strchr()函数,它查找字符串中首次出现指定字符以及其后面的字符。

$string= 'dir/upload.image.jpg'; 
$file_type= strrchr($string, '.'); //取得某字符最后出现处起的字符串。

方法五:pathinfo

$string= 'dir/upload.image.jpg'; 
$arr= pathinfo($string); //返回文件路径的信息 print_r($arr); 
$file_type= $arr['extension'];

方法六:正则表达式

$string= 'dir/upload.image.jpg'; 
eregi('^["."]+$', $string, $arr);//用正则表达式来分割 $count= count($arr); 
$count-=1; 
$file_type= $arr[$count];

方法七:strrev

function get_ext5($file_name){
    return strrev(substr(strrev($file_name), 0, strpos(strrev($file_name), ‘.’)));
}
阅读 2085