正则采集器之五——商品匹配规则

需求设计 + 实现分析

系统通过访问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;
	
	// 淘宝商品id
	private 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 数据

keyvalue
all任意字符串
exclude不包含 某些字符串 的字符串
fix固定字符串
number价格,[\d\.]+

正则单元项html:

 <div
        class="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-select
              v-model="regexpItem.type"
              @change="changeType(regexpItem)"
            >
              <el-option
                v-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-input
                class="match_input"
                v-if="regexpItem.matchType == 'exclude'"
                v-model="regexpItem.matchStr"
              />
              <el-radio value="fix" label="fix">固定</el-radio>
              <el-input
                v-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>
      <div
        class="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-select
              v-model="regexpItem.type"
              @change="changeType(regexpItem)"
            >
              <el-option
                v-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-input
                class="match_input"
                v-if="regexpItem.matchType == 'exclude'"
                v-model="regexpItem.matchStr"
              />
              <el-radio value="fix" label="fix">固定</el-radio>
              <el-input
                v-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'
            break
          case 'pic':
          case 'itemId':
            regexpItem.matchType = 'exclude'
            regexpItem.matchStr = '"'
            break
          case '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.id
      getById(ctx.id).then(res => {
        ctx.itemWebsite = res.data
        if (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博客

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值