- 因为项目有需求,整本小说上传之后自动识别章节目录,然后库存入库。所以就思考如何实际操作。
一、思路
-
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.