文本识别标题后分割_php版

  • 因为项目有需求,整本小说上传之后自动识别章节目录,然后库存入库。所以就思考如何实际操作。

一、思路

  • 1,文件上传,这个基础了。另外,文件上传的大小,在php.ini可以设置,但是最后决定上传的大小的,是postmax的设置。

  • 2,获取文件,然后将其读取,若文件太大,就将其分割

  • 3,正则识别章节标题内容,然后记录下来

  • 4,将文件按行读取匹配章节标题,记录行数

  • 5,按章节标题行数-1,分割文件

  • 6,记录,入库,这样就能完整的得到整本小说的目录,和章节内容

二、文件上传

  • 因为框架自带的文件上传功能,他会把上传的文件按日期随意放置,这样不符合个人严谨的态度。所以就手写了个原生文件上传功能。
 /**
     * 原生上传文件
     * 根据上传的文件名判断文件在该路径是否已经上传
     */
    public static function phpUpload($type,$path){
        //防止乱码
        header("Content-type: text/html; charset=utf-8");
        $file=$type;
        if ($_FILES[$file]['error']>0){    //file 是post上字段名
            return "Error:".$_FILES[$file]['error'];
        }
        //上传文件信息
        $data=[
            'fileName'=>$fileNmae=$_FILES[$file]['name'],
            "type"=>$type=$_FILES[$file]['type'],
            'size'=>$size=($_FILES[$file]['size']/1024)."kb",
            'tmp_name'=>$tmp_name=$_FILES[$file]['tmp_name'],
        ];
        $filepath=$path.DS.$fileNmae;           //文件路径,包含文件名
        $fileNmae=iconv('utf-8','gb2312',$fileNmae);
        if (self::fileIsHas($filepath))return null;   //判断文件是否已经上传
        if (!self::fileIsHas($path)) self::createdir($path);    //没有该文件夹则创建

        move_uploaded_file($tmp_name,$filepath);
        return $filepath;
    }
  • 还有一个filesHsa()函数,这个是判断文件是否存在的方法
  • 这方法没有判断文件上传类型、大小,可以根据个人情况添加

三、文件分割

  • 这里当初思考了两个方法,一个是将文件按规定大小分割,然后缓存起来,再每个读取,再合并。一个是,将文件分割,进行下面步骤,然后再读取。本文取用后者
  • 后来,发现PHP可以多线程,具体多线程可以看PHP多线程
  /**
     * 分段器
     * 默认每2m分段一次
     * filePath		文件路径
     * callback	回调函数,分割完文件,可以在回调函数中进行接下来步骤处理
     * blockSize 分割文件大小,默认是2m
     */
    public static function sectionalizer($filePath,$callback,$blockSize=10485760 / 5){
        if (!is_callable($callback))return false;
        $size=filesize($filePath);  //整体大小
        while ($size>0){
            $residue=bcsub($size,$blockSize,0);
            $callbackSize=$residue>0?$residue:$size;
            $data=$callback()use($residue);    //回调函数,分段期间做的事
            $size=$residue;
        }
        return $data;
    }
  • 这个一个文件大小分割器,可以根据规定大小将文件分割,然后在回调函数中进行接下来步骤

四、正则识别章节标题

 /**
     * 获取整字符内容
     * @param $path
     */
    public static function pregChapters(string $path,string $pattern,int $size){
        $file=fopen($path,'rb')or die("unable open the file");
        $content=fread($file,$size);
        fclose($file);
        //$pattern="/chapter[\s][A-Z]/i";
        preg_match_all($pattern,$content,$result,PREG_PATTERN_ORDER);
        return $result;
    }
  • 该方法可以根据正则获取文本内容
  • php的正则规则有些区别,具体可以看PHP正则
  • 可以根据上个分割器,将文本倒入分割

五、获取章节标题行数

  • 因为按章节标题分割需要获取具体章节标题行号
  /**
     * 返回章节对应的行数
     * @param $path
     * @param array $chapterName
     * @return array
     */
    public static function ChaptersLine($path,array $chapterName){
        $fp=fopen($path,"rb");
        $lines=[];      //每一行
        $num=[];        //每一章节对应的行数
        while (!feof($fp)){
            $lines[]=fgets($fp);
        }
        $i=0;                                   //章节下标
        $chapterName=array_flip($chapterName);  //翻转数组,redis思想
        foreach ($lines as $k=> $line){
            if (empty($line))continue;
            if (isset($chapterName[trim($line)])){       //存在该下标,存在
                $i++;
                $num[]=[
                  'chapter_name'=>$lines[$k],
                  'line_num'=>$k,
                  'chapter_num'=>$i
                ];
            }
        }
        fclose($fp);
        return $num;
    }

  • 当初是想把文本每一行,和章节标题数组进行遍历,但是这时间复杂度就为O(n)^2了
  • 所以就运用了redis、数据格式的思想
    • $chapterName[ ] 、array_flip 翻转过来
    • 在把文本每一行当作$chapterName[ ]数组的健,只要判断是否存在即可,时间复杂度为O(n)

六、根据章节行号分割文件

 /**
     * 分割章节
     * @param $path
     * @param $chapterNum
     * @param $novels_id
     * @throws \think\exception\DbException
     */
    public static function splitByChapter($path,$chapterNum,$novels_id){
        $chapterPath=dirname($path).DS;
        $novels=Novels::get($novels_id);

        $chapterData=[
            'novels_id'=>$novels_id,
            'title'=>$novels->title,
            'created_at'=>time(),
        ];
        $i=0;
        $word_count=0;

        foreach ($chapterNum as $k =>$v){
            $endLine=$v['line_num']-1;
            $content=self::getLine($path,$i,$endLine);                                        //获取行,内容
            self::ChapterTxt($chapterPath.$v['chapter_num'].".txt",$content);       //生成章节文本
            $chapterData['word_count']=help::wordCount($content);                                         //字数统计
            $chapterData['num']=$v['chapter_num'];

            $chapterData['path']=NOVELS_PATH_MYSQL.$novels->title.DS.$v['chapter_num'].".txt";                 //章节目录路径,重组
            $chapter=Chapter::create($chapterData);                                                                  //入章节表
            $chapterManagerData=[
                'chapter_num'=>$v['chapter_num'],
                'title'=>$chapterData['title'],
                'words_num'=>$chapterData['word_count'],
                'createtime'=>time(),
            ];
            ChapterManage::create($chapterManagerData);                                             //后台章节列表页
            HandleRedis::getInstance()->chapterCache($novels_id.":".$chapter->getLastInsID(),json_encode($chapterData)); //种缓存
            $word_count+=$chapterData['word_count'];
            $i=$endLine;
        }
        return $word_count;
    }
 
  • 该方法是遍历章节数组,根据章节行数-1,分割文本,生成txt文件,然后做记录
 /**
     * 根据行数分割文件
     * @param $fileName
     * @param $start
     * @param $limit
     * @return string
     */
    public static function getLine($fileName,$start,$limit){
        $f= new \SplFileObject($fileName,'r');
        $f->seek($start);   //文件指针指向行数
        $ret='';
        for ($i=0;$i<$limit;$i++){
            $ret.=$f->current();    //内容
            $f->next();             //下一行
        }
        return $ret;
    }
  • 该方法是根据文本行数,将文本进行分割,返回分割文本内容
/**
     * 生成章节文件
     * @param $chapterPath
     * @param $content
     * @return false|mixed
     */
    public static function ChapterTxt($chapterPath,$content){
        if (help::fileIsHas($chapterPath))return false;
        $fp=fopen($chapterPath,'wb')or die(__("can't open this file :".$chapterPath));
        fwrite($fp,$content);
        fclose($fp);
        return $chapterPath;
    }
  • 该方法是根据文本内容生成txt文件导出,返回路径
  • txt文件导出,其实就是写入文件,若文件不存在则会新建
  • fopen中 wb 的b ,意思是window,和linux写入一致格式,不然会window文本放到linux系统会有文本内容win格式问题

七、入库、做记录

  • 整体调用总函数
 /**
     * 上传整本小说,分割章节,然后更新库
     * @param string $path
     * @param array $novels_id
     * @param string $patten
     * @throws \think\exception\DbException
     */
    public static function wholeNovelsChapters(string $path,int $novels_id,string $patten="/chapter[\s][A-Z]+\./i"){
        /*$dir=novels_opeate::sectionalizer($path,function($size)use($path,$patten,$dir){
            $dir[]=novels_opeate::pregChapters($path,$patten,$size);    //分段将目录导入dir数组内
            return $dir;
        });*/
        $dir=novels_opeate::pregChapters($path,$patten,filesize($path))[0];                     //获取章节目录
        $chaptersNum=novels_opeate::ChaptersLine($path,$dir);                                   //获取章节目录所在的行数
        $word_count=novels_opeate::splitByChapter($path,$chaptersNum,$novels_id);                //分割整本书,生成章节txt文件,入库
        $chaptersNum=array_sum($chaptersNum);
        novels_model::where('id',$novels_id)                                                //更新小说表
            ->update([
                'path'=>dirname($path).DS,
                'chapter_count'=>$chaptersNum,                                                  //小说章节总数
                'word_count'=>$word_count,
            ]);
        unlink($path);                                                                          //删除原本
    }

  • 当初实现时候思维比较混乱,时间仓促没有使用分割器,后期可以优化
  • 其实用多线程的话,性能会大幅度提升,而且更加简便,后期有兴趣可以试试
  • 本文有个弊端,就是文本标题是正则选取,如果上传的文本章节标题和正则不相符,则会失去作用,所以需要提供具体章节标题
  • 本文使用的正则规则,“/chapter[\s][A-Z]+./i”,可以识别CHAPTER II.
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值