需求设计 + 实现分析
系统通过访问URL得到html代码,通过正则表达式匹配html,通过反向引用来得到商品的标题、图片、价格、原价、id,这部分逻辑在java中实现。
匹配商品的正则做成可视化编辑,因为不同网站的结构不同,同一个网站的结构会随时间发生变化,为方便修改,做成可视化编辑。以九块邮为例分析匹配商品的正则:
由此图可见一个正则由多个单元项组成,每个单元项都是一个单独的正则(包括匹配商品的字段项和字段项前后的标志字符串),比如匹配价格的[\d\.]+,价格前面的html >¥ 。最终组合成的正则需要能够正确解析出一个个商品的标题、图片、价格、原价和id字段。
后端代码
匹配代码
package com.learn.reptile.utils;import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.learn.reptile.entity.po.Item;import cn.hutool.http.HttpUtil;public class ItemCraw {/*** 通过url获取html,然后解析出出商品* @param url* @param regexpStr 商品匹配正则表达式* @param startStr 开始匹配字符串* @param endStr 结束匹配字符串* @return*/public static List<Item> parseItemsFromUrl(String url, String regexpStr, String startStr, String endStr) {String html = HttpUtil.get(url);if(StringUtils.isNotBlank(endStr)) {html = html.substring(html.indexOf(startStr), html.lastIndexOf(endStr));} else {html = html.substring(html.indexOf(startStr));}List<Item> items = new ArrayList<>();Pattern pattern = Pattern.compile(regexpStr);Matcher matcher = pattern.matcher(html);// 每一个匹配整体while(matcher.find()) {Item item = new Item();item.setItemId(matcher.group("id"));item.setPic(matcher.group("pic"));item.setTitle(matcher.group("title"));item.setPrice(Double.parseDouble(matcher.group("price")));item.setPrePrice(Double.parseDouble(matcher.group("prePrice")));items.add(item);}return items;}}
匹配结果实体类
package com.learn.reptile.entity.po;import java.util.Date;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;import lombok.Data;@Data
public class Item {@TableId(type = IdType.AUTO)private Long id;// 淘宝商品idprivate String itemId;// 来源,匹配网站的编码private String source;private String title;private String pic;private double price;private double prePrice;// 采集时间private Date createTime;
}
controller类
package com.learn.reptile.web.controller;import java.util.List;import javax.annotation.Resource;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import com.learn.reptile.entity.po.Item;
import com.learn.reptile.entity.po.ItemWebsite;
import com.learn.reptile.entity.vo.R;
import com.learn.reptile.utils.ItemCraw;@RequestMapping("/item")
@RestController
public class ItemController {@PostMapping("test")public R<List<Item>> test(@RequestBody ItemWebsite itemWebsite) {return R.ok(ItemCraw.parseItemsFromUrl(itemWebsite.getUrl(), itemWebsite.getRegexpStr(), itemWebsite.getStartStr(), itemWebsite.getEndStr()));}
}
前端代码
添加router,位置:src/router/modules/home.js。router的path中增加了参数:id,即网站的id。
{path: '/item',component: Layout,name: 'item',meta: {title: '商品',},icon: 'icon-home',children: [{path: 'itemWebsite',name: 'itemWebiste',component: () => import('@/views/item_website/index.vue'),meta: {title: '网站',},},{path: 'itemRegexp/:id',name: 'itemRegexp',component: () => import('@/views/item_website/regexp.vue'),meta: {title: '商品匹配正则',},hidden: true,},],},
位置src/views/item_website/regexp.vue
分析
用regexpItems表示正则单元项列表,每个regexpItem包含三个字段:type 表示匹配商品的某个字段还是仅仅是分隔部分,matchType 表示该部分的正则模式,matchStr 表示该正则模式需要用到的字符串。
type 数据
key | value |
id | 商品id |
title | 标题 |
pic | 图片 |
price | 价格 |
prePrice | 原价 |
其他 |
matchType 数据
key | value |
all | 任意字符串 |
exclude | 不包含 某些字符串 的字符串 |
fix | 固定字符串 |
number | 价格,[\d\.]+ |
正则单元项html:
<divclass="regexp_item"v-for="(regexpItem, index) in regexpItems":key="index">{{ index + 1 }}<el-icon @click="regexpItems.splice(index, 1)"><CloseBold /></el-icon><div class="line"><div class="label">类型</div><div class="field"><el-selectv-model="regexpItem.type"@change="changeType(regexpItem)"><el-optionv-for="(name, code) in types":key="code":value="code":label="name">{{ name }}</el-option></el-select></div></div><div class="line"><div class="label">匹配类型</div><div class="field"><el-radio-group v-model="regexpItem.matchType"><el-radio value="number" label="number">数值</el-radio><el-radio value="all" label="all">任意字符</el-radio><el-radio value="exclude" label="exclude">除</el-radio><el-inputclass="match_input"v-if="regexpItem.matchType == 'exclude'"v-model="regexpItem.matchStr"/><el-radio value="fix" label="fix">固定</el-radio><el-inputv-if="regexpItem.matchType == 'fix'"v-model="regexpItem.matchStr"/></el-radio-group></div></div></div>
页面整体布局为左中右结构,左侧是正则单元项列表,中间是操作按钮,右边是测试匹配结果,完整html部分代码如下:
<template><div style="margin: 10px;">{{ itemWebsite.name }}匹配规则</div><div style="display: flex;"><div style="width: 60%"><div class="form"><div class="form_label">匹配开始字符串</div><div class="form_field"><el-input v-model="itemWebsite.startStr"></el-input></div><div class="form_label">匹配结束字符串</div><div class="form_field"><el-input v-model="itemWebsite.endStr"></el-input></div></div><divclass="regexp_item"v-for="(regexpItem, index) in regexpItems":key="index">{{ index + 1 }}<el-icon @click="regexpItems.splice(index, 1)"><CloseBold /></el-icon><div class="line"><div class="label">类型</div><div class="field"><el-selectv-model="regexpItem.type"@change="changeType(regexpItem)"><el-optionv-for="(name, code) in types":key="code":value="code":label="name">{{ name }}</el-option></el-select></div></div><div class="line"><div class="label">匹配类型</div><div class="field"><el-radio-group v-model="regexpItem.matchType"><el-radio value="number" label="number">数值</el-radio><el-radio value="all" label="all">任意字符</el-radio><el-radio value="exclude" label="exclude">除</el-radio><el-inputclass="match_input"v-if="regexpItem.matchType == 'exclude'"v-model="regexpItem.matchStr"/><el-radio value="fix" label="fix">固定</el-radio><el-inputv-if="regexpItem.matchType == 'fix'"v-model="regexpItem.matchStr"/></el-radio-group></div></div></div></div><div style="width: 180px; text-align: center;"><div style="margin-bottom: 10px;"><el-button round type="primary" @click="add">增加匹配项</el-button></div><div style="margin-bottom: 10px;"><el-button type="primary" round @click="save">保存</el-button></div><el-button type="primary" round @click="test">测试</el-button></div><div style="width: 40%;"><pre>{{ resultItems }}</pre></div></div>
</template>
javascript部分:
import {getCurrentInstance,reactive,toRefs,ref,computed,watch,onMounted,
} from 'vue'
import { getById, update } from '@/api/itemWebsite'
import { test } from '@/api/item'
import { ElMessageBox } from 'element-plus'export default {setup() {const { proxy: ctx } = getCurrentInstance()const state = reactive({id: '',itemWebsite: {},regexpItems: [],types: {title: '标题',pic: '图片',id: '商品id',price: '价格',prePrice: '原价','': '其他',},resultItems: '',add() {ElMessageBox.prompt('请输入添加的位置下标', '添加匹配项', {inputPattern: /\d+/,inputErrorMessage: '下标必须为正整数',}).then(({ value }) => {const index = parseInt(value)ctx.regexpItems.splice(index - 1, 0, {type: '',matchType: '',matchStr: '',})})},changeType(regexpItem) {switch (regexpItem.type) {case 'price':case 'prePrice':regexpItem.matchType = 'number'breakcase 'pic':case 'itemId':regexpItem.matchType = 'exclude'regexpItem.matchStr = '"'breakcase 'title':regexpItem.matchType = 'exclude'regexpItem.matchStr = '<'}},save() {var regexpStr = '' // 通过正则单元项列表生成正则字符串ctx.regexpItems.forEach(item => {var str = ''if (item.matchType == 'all') {str = '.+?'} else if (item.matchType == 'exclude') {str = '[^' + item.matchStr + ']+'} else if (item.matchType == 'fix') {str = item.matchStr} else if (item.matchType == 'number') {str = '[\\d\\.]+'}if (item.type) {regexpStr += '(?<' + item.type + '>' + str + ')'} else {regexpStr += str}})update({startStr: ctx.itemWebsite.startStr,endStr: ctx.itemWebsite.endStr,regexpContents: JSON.stringify(ctx.regexpItems), // 正则单元项列表以json字符串保存regexpStr: regexpStr,id: ctx.id,}).then(res => {ctx.$message.success('保存成功')})},test() {var regexpStr = ''ctx.regexpItems.forEach(item => {var str = ''if (item.matchType == 'all') {str = '.+?'} else if (item.matchType == 'exclude') {str = '[^' + item.matchStr + ']+'} else if (item.matchType == 'fix') {str = item.matchStr} else if (item.matchType == 'number') {str = '[\\d\\.]+'}if (item.type) {regexpStr += '(?<' + item.type + '>' + str + ')'} else {regexpStr += str}})test({url: ctx.itemWebsite.url,startStr: ctx.itemWebsite.startStr,endStr: ctx.itemWebsite.endStr,regexpStr: regexpStr,}).then(res => {ctx.$message.success('测试成功')ctx.resultItems = JSON.stringify(res.data,['itemId', 'title', 'pic', 'price', 'prePrice'],'\t')})},})onMounted(() => {ctx.id = ctx.$route.params.idgetById(ctx.id).then(res => {ctx.itemWebsite = res.dataif (ctx.itemWebsite.regexpContents) {ctx.regexpItems = eval('(' + ctx.itemWebsite.regexpContents + ')')}})})return {...toRefs(state),}},
}
样式部分:
<style>
.regexp_item {margin: 10px;border-top: 1px solid gray;border-right: 1px solid gray;position: relative;width: 100%;
}
.regexp_item .el-icon {position: absolute;right: -5px;top: -5px;color: red;cursor: pointer;
}
.line {display: flex;
}
.line > div {border-bottom: 1px solid gray;border-left: 1px solid gray;padding: 5px;
}
.label {width: 30%;
}
.field {width: 70%;
}
.match_input {width: 100px;margin-right: 15px;
}
.form {display: flex;align-items: center;margin: 10px;width: 100%;
}
.form_label {width: 20%;margin-left: 20px;
}
.form_field {width: 30%;
}
</style>
代码及演示网站见:正则采集器之一——需求说明-CSDN博客