文字替换


// 替换
public function replace(Request $request, $bid){
$cond = $request->only('id', 'keyword', 'show_positions');
// var_dump($cond);die;
return Api::success($this->charpterService->search($cond['id'], $cond['keyword'],1));
}

// 替换 收
public function contentreplace(Request $request, $bid){
// var_dump($request);die;
$cond = $request->only('id', 'keyword', 'new_keyword', 'chapters');

return Api::success($this->charpterService->contentReplace($cond['id'], $cond['keyword'], $cond['new_keyword'], $cond['chapters']));
}



// 搜索关键词 发
public function search($id, $keyword, $showPositions = false)
{
if (!$keyword) {
throw new Exception("关键词不能为空");
}

$chapterResults = [];
$keywordCount = 0;

// 遍历章节
$this->searchChapters($id, function ($chapter) use ($keyword, $showPositions, &$keywordCount, &$chapterResults) {
// 将章节内容分段匹配
$this->matchContent($chapter->content, $keyword, function ($line, $lineContent, $matchCount) use ($keyword, $showPositions, &$keywordCount, &$chapterResults, $chapter) {
// 匹配成功的回调
$currentData = ["line" => $line, "content" => $lineContent];

if ($showPositions) {
$currentData["positions"] = $this->matchPosition($lineContent, $matchCount, $keyword);
}

$chapterResults[$chapter->id][] = $currentData;
$keywordCount += $matchCount;
});
});

return [
"chapter_count" => count($chapterResults),
"keyword_count" => $keywordCount,
"chapters" => $chapterResults
];
}

// 搜索章节
public function searchChapters($id, $callback, $chapterIds = [])
{
$query = Charpter::where("bid", $id)->where('status', 1)->orderBy('sort');

if ($chapterIds) {
$query = $query->whereIn("id", $chapterIds);
}

$query->chunkById(200, function ($chapters) use ($callback) {
foreach ($chapters as $chapter) {
$callback($chapter);
}
});
}

// 分段匹配内容
public function matchContent($content, $keyword, $callback)
{
// 按照换行符分段
$contents = explode("\n", $content);

foreach ($contents as $line => $lineContent) {
// 不区分大小写匹配
$matchCount = mb_substr_count(strtolower($lineContent), strtolower($keyword));

// 如果匹配到了
if ($matchCount) {
$callback($line, $lineContent, $matchCount);
}
}
}

// 匹配位置
public function matchPosition($content, $count, $keyword)
{
$positions = [];
$pos = 0;

for ($i = 0; $i < $count; $i++) {
$pos = mb_stripos($content, $keyword, $pos);
$positions[] = $pos;
$pos = $pos + 1;
}

return implode(",", $positions);
}

// 章节内容替换 收
public function contentReplace($id, $keyword, $newKeyword, $chapters)
{
if (!$keyword) {
throw new Exception("关键词不能为空");
}

$chapters = json_decode($chapters, true);

// 调整结构方便获取对应行号的忽略位置
$chaptersParams = [];
foreach ($chapters as $chapterId => $chapterLines) {
foreach ($chapterLines as $line) {
$chaptersParams[$chapterId][$line["line"]] = $line["ignore"];
}
}

return $this->asyncContentReplace($id, $keyword, $newKeyword, $chaptersParams);
}

// 异步替换
public function asyncContentReplace($id, $keyword, $newKeyword, $chaptersParams)
{
$chapterIds = array_keys($chaptersParams);

// 计算关键词的长度、新关键词的长度以及新关键词与关键词的差值
$keywordLen = mb_strlen($keyword);
$incrLen = mb_strlen($newKeyword) - $keywordLen;

// 遍历替换的章节
$this->searchChapters($id, function ($chapter) use ($keyword, $chaptersParams, $newKeyword, $keywordLen, $incrLen) {
// 遍历替换的段落
$this->matchContent($chapter->content, $keyword, function ($line, $lineContent, $matchCount) use ($chaptersParams, &$chapter, $keyword, $newKeyword, $keywordLen, $incrLen) {
// 记录段落在整体内容的起始结束位置,替换完关键词整体替换掉章节里对应的段落
$lineStartPos = mb_stripos($chapter->content, $lineContent);
$lineLen = mb_strlen($lineContent);

// 计算【匹配出来当前段落的所有位置】【忽略匹配的位置】【需要替换的位置】
$linePos = explode(",", $this->matchPosition($lineContent, $matchCount, $keyword));
$ignorePos = explode(",", $chaptersParams[$chapter->id][$line]);
$replacePos = array_values(array_diff($linePos, $ignorePos));

// 替换需要替换的关键词
foreach ($replacePos as $replaceCount => $pos) {
// 每执行替换一次原来的位置需要根据新的段落偏移
$pos = $pos + ($replaceCount * $incrLen);
$lineContent = Tools::mbSubstrReplace($lineContent, $newKeyword, $pos, $keywordLen);
}

// 整段替换掉原始章节内容
$chapter->content = Tools::mbSubstrReplace($chapter->content, $lineContent, $lineStartPos, $lineLen);
});

// 章节更新处理(重新计算字数)
// $chapter = $this->updateword($chapter, $chapter->content);
$chapter->words = mb_strlen(str_replace(PHP_EOL, '', str_replace(" ", "", trim($chapter->content))));
$chapter->save();

}, $chapterIds);

// 更新书籍总字数
$this->updateword($id);
return true;
}

//更新总字数
public function updateword($id)
{
$sum = Charpter::where('bid', $id)->where('status',1)->sum('words');
// $cnum = Charpter::where('bid',$id)->count();
$find2 = Book::find($id);
$find2->update_status = 1;
$find2->fnum = $sum;
// $find2->cnum = $cnum;
return $find2->save();
}

// 多字节字符串替换
static public function mbSubstrReplace($string, $replacement, $start, $length = null, $encoding = null)
{
if (extension_loaded('mbstring') === true)
{
$string_length = (is_null($encoding) === true) ? mb_strlen($string) : mb_strlen($string, $encoding);

if ($start < 0)
{
$start = max(0, $string_length + $start);
}

else if ($start > $string_length)
{
$start = $string_length;
}

if ($length < 0)
{
$length = max(0, $string_length - $start + $length);
}

else if ((is_null($length) === true) || ($length > $string_length))
{
$length = $string_length;
}

if (($start + $length) > $string_length)
{
$length = $string_length - $start;
}

if (is_null($encoding) === true)
{
return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length);
}

return mb_substr($string, 0, $start, $encoding) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length, $encoding);
}

return (is_null($length) === true) ? substr_replace($string, $replacement, $start) : substr_replace($string, $replacement, $start, $length);
}



vue

<template>
<el-drawer
ref="editWindow"
title="内容替换"
:visible.sync="isOpen"
direction="rtl"
size="80%"
:destroy-on-close="true"
:before-close="close"
:append-to-body="true"
>
<div class="app-container" style="padding-top: 0px">
<el-form :inline="true" :model="listQuery" class="form-inline" label-width="120px" label-position="top">
<el-row>
<el-col :span="11">
<el-form-item label="" size="mini">
<el-input
v-model="listQuery.keyword"
:autofocus="true"
clearable
placeholder="请输入关键词"
style="width: 180px"
size="mini"
@keyup.enter.native="handleFilter"
/>
</el-form-item>
<el-form-item label="" size="mini">
<el-button :disabled="!listQuery.keyword" class="filter-item" type="primary" size="mini"
icon="el-icon-search" @click="handleFilter">
查询相关记录
</el-button>
</el-form-item>

</el-col>
<el-col :span="8" class="left">
<!-- <el-form-item label="" size="mini">
<span>{{ listQuery.id }}</span>
</el-form-item> -->
<el-form-item label="" size="mini">
<span>
命中
<span class="num">
{{ chapter_count }}
</span>
章节;
</span>
</el-form-item>
<el-form-item label="" size="mini">
<span>
出现
<span class="num">
{{ keyword_count }}
</span>
处:
</span>
</el-form-item>
</el-col>
<el-col :span="5" class="right">
<el-form-item label="" size="mini">
<el-button :disabled="!listQuery.keyword || !chapter_count || !temp_count" class="filter-item"
type="danger" size="mini" icon="el-icon-upload2" @click="replaceConfirm">
确认替换
</el-button>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="11">
<el-form-item label="" size="mini">
<el-input
v-model="listQuery.changeword"
clearable
:disabled="!listQuery.keyword"
placeholder="替换为"
style="width: 180px"
size="mini"
@keyup.enter.native="handleFilter"
/>
</el-form-item>
<el-form-item label="" size="mini">
<el-button :disabled="!listQuery.changeword" class="filter-item" type="success" size="mini"
icon="el-icon-view" @click="replacePreview">
预览替换效果
</el-button>
</el-form-item>
<el-form-item label="" size="mini">
<el-checkbox v-model="checked">对比显示</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="8" class="left">
<el-form-item label="" size="mini">
<span>
待替换
<span class="num">
{{ temp_count }}
</span>
处、
</span>
</el-form-item>
<el-form-item label="" size="mini">
<span>
忽略
<span class="num">
{{ ignore_count }}
</span>

</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-table
:loading="listLoading"
:height="clientHeight"
:data="displayData"
fit
style="width: 100%;"
:row-class-name="tableRowClassName"
>
<el-table-column
type="index"
width="50"
/>
<el-table-column label="章节id" align="left" width="90">
<template slot-scope="{row}">
<span>{{ row.chapter_id }}</span>
</template>
</el-table-column>
<el-table-column sort-by="String" label="行数" align="left" width="90">
<template slot-scope="{row}">
<span>{{ row.line }}</span>
</template>
</el-table-column>
<el-table-column label="位置" align="left" width="90">
<template slot-scope="{row}">
<span>{{ row.positions }}</span>
</template>
</el-table-column>
<el-table-column label="内容" align="left">
<template slot-scope="{row}">
<!-- <span v-html="row.content">{{ row.content }}</span> -->
<span v-html="row.contentHtml"/>
</template>
</el-table-column>
<el-table-column label="操作" align="left" class-name="small-padding fixed-width" width="80">
<template slot-scope="{row}">
<el-link v-if="!row.ignore" type="primary" @click="ignoreClick(row)">此次忽略</el-link>
<el-link v-else type="danger" @click="ignoreCancelClick(row)">加入待替换</el-link>
</template>
</el-table-column>
</el-table>

<pagination v-show="total>0" :total="total" :page.sync="page" :limit.sync="page_size"
@pagination="paginCurrentChange"/>

</div>
</el-drawer>
</template>

<script>
import request from '@/utils/request'
import Pagination from '@/components/Pagination'

export default {
name: 'ReplaceCom',
components: {Pagination},
filters: {
statusFilter(status) {
const statusMap = {
'已入库': '',
'未入库': 'info',
'已上架': 'success',
'未上架': 'warning'
}
return statusMap[status]
}
},
data() {
return {
isOpen: false,
bid: 0,
checked: false,
displayData: [],
chapter_count: 0,
temp_count: 0,
keyword_count: 0,
ignore_count: 0,
loading: false,
tableKey: 0,
chapters: null,
total: 0,
listLoading: false,
page: 1,
page_size: 50,
listQuery: {
id: 0,
keyword: '',
changeword: '',
show_positions: 1
},
temp: {
id: undefined
}
}
},
computed: {
clientHeight() {
return document.documentElement.clientHeight - 125
}
},
watch: {
bookid(val) {
this.listQuery.id = val
this.listQuery.keyword = ''
this.ignore = []
},
checked() {
this.page = 1
this.tableList()
}
},
methods: {
init(bid) {
this.bookid = bid
this.listQuery.id = this.bookid
this.isOpen = true
},
close(done) {
this.unsetdata()
this.isOpen = false
this.$emit('complete')
done()
},
unsetdata() {
this.listQuery = {}
this.bookid = ''
this.temp = ''
this.checked = false
this.displayData = []
this.chapter_count = 0
this.temp_count = 0
this.keyword_count = 0
this.ignore_count = 0
this.loading = false
this.tableKey = 0
this.chapters = null
this.total = 0
this.listLoading = false
},
replaceByPos(content, keyword, pos, changeword) {
const _arr = content.split('')
// 数组切割
// 替换为 html ,标示出选中的词,以及 替换的词
if (!changeword) {
_arr.splice(pos, keyword.length, ` <span style='color:#fff;background:#7C4027;text-decoration:line-through;'>${keyword}</span> `)
} else {
if (this.checked) {
_arr.splice(pos, keyword.length, ` <span style='color:#fff;background:#5B191E;text-decoration:line-through;'>${keyword}</span> -> <span style='color:#fff;background:#3B442E;'>${changeword}</span> `)
} else {
_arr.splice(pos, keyword.length, ` <span style='color:#fff;background:#3B442E;'>${changeword}</span> `)
}
}
// console.log(_arr.join(''))
// console.log(_arr.splice(pos, keyword.length,changeword))
return _arr.join('')
},
tableRowClassName({row, rowIndex}) {
if (row.ignore === true) {
return 'success-row'
}
return ''
},
tableList() {
// this.displayData是当前页面要显示的数据
this.displayData = []
const _temp = []
for (
// pagesize是当前页面要显示总条数,例如:每页显示20条;page是当前页面数;
var j = this.page_size * (this.page - 1); j < this.page_size * this.page; j++) {
// this.chapters是总数据
if (this.chapters[j]) {
if (this.chapters[j].ignore) {
this.chapters[j].contentHtml = this.chapters[j].content
} else {
this.chapters[j].contentHtml = this.replaceByPos(this.chapters[j].content, this.listQuery.keyword, this.chapters[j].positions, this.listQuery.changeword)
}
_temp.push(this.chapters[j])
}
}
this.$nextTick(() => {
// 解决前端翻页后,滚动条没有回到顶部问题
this.displayData = [].concat(_temp)
})
},

// 获取当前页
paginCurrentChange(pObj) {
this.page = pObj.page
this.tableList()
},
replaceConfirm() {
console.log('replaceConfirm')

if (!this.listQuery.changeword) {
this.$confirm(`未输入替换文本,【${this.listQuery.keyword}】将替换为空,确认操作吗?`, '替换为空 操作提示', {
confirmButtonText: '确定替换为空',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
// 此处请求修改接口
this.replaceSubmit()
})
.catch(err => {
console.error(err)
})
} else {
this.$confirm(`【${this.listQuery.keyword}】将替换为【${this.listQuery.changeword}】,确认操作吗?`, '替换 操作提示', {
confirmButtonText: '确定替换',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
// 此处请求修改接口
this.replaceSubmit()
})
.catch(err => {
console.error(err)
})
}
},
replaceSubmit() {
// 提交此次替换操作
// console.log(this.chapters)
this.listLoading = false
const _data = {keyword: this.listQuery.keyword, new_keyword: this.listQuery.changeword || '', chapters: {}}

// 将数据处理为接口需要的结构
for (let index = 0; index < this.chapters.length; index++) {
const element = this.chapters[index]
if (!_data.chapters[element.chapter_id]) {
_data.chapters[element.chapter_id] = []
}
// 已存在该 chapter id 去找line
const _arrLs = _data.chapters[element.chapter_id]
let lineExistFlag = false // 是否不存在此line标示
for (let index = 0; index < _arrLs.length; index++) {
const eL = _arrLs[index]
if (element.line === eL.line) {
// 找到了 line ,处理 ignore
if (element.ignore) {
if (eL.ignore) {
eL.ignore += ',' + element.positions
} else {
eL.ignore = element.positions
}
// 处理完 退出此次for循环
lineExistFlag = true
break
}
lineExistFlag = true
}
}
if (!lineExistFlag) {
// 未找到line
// todo
_data.chapters[element.chapter_id].push({line: element.line, ignore: element.ignore ? element.positions : ''})
}
}
_data.id = this.bookid
console.log('_data', _data)
// 此处请求接口,修改数据
_data.chapters = JSON.stringify(_data.chapters)
request({
url: '/xiaoshuo/charpter/' + this.bookid + '/contentreplace/v1',
method: 'put',
data: _data
}).then(res => {
// console.log(res)
this.$message({
type: 'success',
message: '操作成功,数据处理中……'
})
setTimeout(() => {
this.listLoading = false
this.close()
}, 2000)
}).catch(e => {
this.listLoading = false
console.log(e)
})
},
replacePreview() {
console.log('replacePreview')
if (!this.chapters || this.chapters.lengh === 0) {
this.$message.info('请先查询记录')
return
}
this.page = 1
this.tableList()
},
ignoreClick(row) {
row.ignore = true
// this.chapters = [].concat(this.chapters)

row.contentHtml = row.content

this.displayData = [].concat(this.displayData)
this.temp_count--
this.ignore_count++
this.$message.success('操作成功!')
// this.tableList()
},
ignoreCancelClick(row) {
row.ignore = false
this.temp_count++
this.ignore_count--
// this.chapters = [].concat(this.chapters)
row.contentHtml = this.replaceByPos(row.content, this.listQuery.keyword, row.positions, this.listQuery.changeword)
this.displayData = [].concat(this.displayData)
this.$message.success('操作成功!')
// this.tableList()
// this.ignore.splice(this.ignore.indexOf())
},
getList() {
this.listLoading = true
this.listQuery.id = this.bookid
// getKeyWordsList(this.listQuery)
request({
url: '/xiaoshuo/charpter/' + this.bookid + '/replace/v1',
method: 'get',
params: Object.assign(this.listQuery)
}).then(response => {
const _temp = []
for (const key in response.data.chapters) {
for (let index = 0; index < response.data.chapters[key].length; index++) {
const element = response.data.chapters[key][index]
element.chapter_id = key
if (element.positions.indexOf(',') !== -1) {
// 如果有同一行有多个位置,继续拆分
const _posArr = element.positions.split(',')
for (let indexP = 0; indexP < _posArr.length; indexP++) {
const eP = Object.assign({}, element)
eP.positions = _posArr[indexP]
eP.contentHtml = this.replaceByPos(eP.content, this.listQuery.keyword, eP.positions, this.listQuery.changeword)
_temp.push(eP)
}
} else {
element.contentHtml = this.replaceByPos(element.content, this.listQuery.keyword, element.positions, this.listQuery.changeword)
_temp.push(element)
}
}
}
this.chapters = _temp
this.temp_count = _temp.length
this.total = _temp.length
this.chapter_count = response.data.chapter_count
this.keyword_count = response.data.keyword_count
// this.total = response.data.total

// 前端分页
this.page = 1
this.tableList()

this.listLoading = false
}).catch(e => {
console.log(e)
this.listLoading = false
})
},
handleFilter() {
// this.listQuery.page = 1
this.getList()
},
resetTemp() {
this.temp = {
id: undefined,
keyword: ''
}
}
}
}
</script>
<style>
.has-item {
padding-left: 20px;
font-size: 14px;
color: #606266;
font-weight: bold;
}

.has-item .label {
padding-right: 12px;
}

.el-dialog__body {
padding: 0 20px;
}

.right {
text-align: right;
}

.center {
text-align: center;
}

.num {
font-size: 18px;
}

.bg {
background: transparent !important;
}

.el-table .warning-row {
background: oldlace;
}

.el-table .success-row {
background: #f0f9eb;
}
</style>



posted @ 2024-03-27 14:27  哼哼哈兮啊  阅读(1)  评论(0编辑  收藏  举报