好客租房-13.WebMagic

news2025/1/15 17:51:09

13. 项目接入ES

编写爬虫抓取房源数据

开发搜索房源接口服务

整合前端开发实现搜索功能

优化搜索功能增加高亮和分页功能

热词推荐功能实现

拼音分词

13.1 制作假数据

13.1.1 WebMagic抓取数据

为了丰富我们的房源数据,所以我们采用WebMagic来抓取一些数据,目标网站是上海链家网。

打开it-es项目

        <!---引入WebMagic依赖-->
		<dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.3</version>
        </dependency>
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.3</version>
        </dependency>
        <!---引入commons-io依赖-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

我们先写一个官方demo来快速学习下webmagic.

package cn.itcast.es.wm;

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;

/**
 * 官方实例demo
 */
public class GithubRepoPageProcessor implements PageProcessor {
    // 爬虫策略
    private Site site = Site.me()
            .setRetryTimes(3)   // 下次失败后,重试三次
            .setSleepTime(1000) // 每隔一秒,请求下一个网页
            .setTimeOut(10000); // 下载10s则超时失败.

    @Override
    public void process(Page page) {
        // 新增解析多个目标网址
        page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/[\\w\\-]+/[\\w\\-]+)").all());
        page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/[\\w\\-])").all());
        // 目标解析域为 https://github.com/(\\w+)/的所有样式
        page.putField("author", page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
        // 当前页面的h1并且样式为class='entry-title public'下的<strong>标签下的<a>标签里的文字.
        page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
        // 跳过分页列表所在的页(就是分页列表页面没有name属性, 只有详情页才有name属性)
        if (page.getResultItems().get("name") == null) {
            //skip this page
            page.setSkip(true);
        }
        page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));
    }

    @Override
    public Site getSite() {
        return site;
    }
    // 哦对了, 运行会报错, 是正常现象, 官方demo就这样.
    public static void main(String[] args) {
        // 指定目标网址
        Spider.create(new GithubRepoPageProcessor()).addUrl("https://github.com/code4craft").thread(5).run();
    }
}

多说无益, 我们直接新增一个LianjiaPageProcessor来解析链家网页吧.

目标网页:

https://sh.lianjia.com/zufang/pg1
pg1是第一页, 所以我们改变1->100就是遍历100页了.
package cn.itcast.es.wm;

import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;

import java.util.List;

/**
 * 解析上海链家分页租房内容前100页
 * https://sh.lianjia.com/zufang/pg{1...100}
 */
public class LianjiaPageProcessor implements PageProcessor {
    // 下载失败后重试次数为三次, 每隔200ms发起一次请求.
    private Site site = Site.me().setRetryTimes(3).setSleepTime(200);

    @Override
    public void process(Page page) {
        Html html = page.getHtml();
        // 得到房源分页列表中所有的详情链接, 根据css选择器进行选择
        List<String> all = html.css(".content__list--item--title a").links().all();
        // 所有详情的链接被加入到下一次爬取内容中
        page.addTargetRequests(all);

        // 解析详情页的属性到page中.
        toResolveFiled(page, html);

        // 只有入口页没有title
        if (page.getResultItems().get("title") == null) {
            // 不下载本页
            page.setSkip(true);
            //分页
            for (int i = 1; i <= 100; i++) {
                page.addTargetRequest("https://sh.lianjia.com/zufang/pg" + i);
            }
        }
    }

    private void toResolveFiled(Page page, Html html) {
        // 这里采用的统一为css定位解析内容
        String title = html.xpath("//div[@class='content clear w1150']/p/text()").toString();
        page.putField("title", title);
        String rent = html.xpath("//div[@class='content__aside--title']/span/text()").toString();
        page.putField("rent", rent);
        String type = html.xpath("//ul[@class='content__aside__list']/allText()").toString();
        page.putField("type", type);
        String info = html.xpath("//div[@class='content__article__info']/allText()").toString();
        page.putField("info", info);
        String img = html.xpath("//div[@class='content__article__slide__item']/img").toString();
        page.putField("img", img);
//        String printInfo = page.getRequest().getUrl() + "\ntitle = " + title + "\n" + "rent" + rent + "\n"
//                + "type" + type + "\n" + "info" + info + "\n" + "img" + img;
//        System.out.println(printInfo);
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {
        // 爬虫标记入口页面,五个线程工作.
        Spider.create(new LianjiaPageProcessor())
                .addUrl("https://sh.lianjia.com/zufang/")
                .thread(5)
                // 对下载好的页面使用自定义的MyPipeline类进行流处理
                .addPipeline(new MyPipeline())
                .run();
    }
}

MyPipeLine类如下:

package cn.itcast.es.wm;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

public class MyPipeline implements Pipeline {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void process(ResultItems resultItems, Task task) {
        Map<String, Object> data = new HashMap<>();
        data.put("url", resultItems.getRequest().getUrl());
        data.put("title", resultItems.get("title"));//标题
        data.put("rent", resultItems.get("rent"));//租金
        String[] types = StringUtils.split(resultItems.get("type"), ' ');
        data.put("rentMethod", types[0]);//租赁方式
        data.put("houseType", types[1]);//户型,如:2室1厅1卫
        data.put("orientation", types[2]);//朝向
        String[] infos = StringUtils.split(resultItems.get("info"), ' ');
        for (String info : infos) {
            if (StringUtils.startsWith(info, "看房:")) {
                data.put("time", StringUtils.split(info, ':')[1]);
            } else if (StringUtils.startsWith(info, "楼层:")) {
                data.put("floor", StringUtils.split(info, ':')[1]);
            }
        }
        String imageUrl = StringUtils.split(resultItems.get("img"), '"')[3];
        String newName = StringUtils
                .substringBefore(StringUtils
                        .substringAfterLast(resultItems.getRequest().getUrl(),
                                "/"), ".") + ".jpg";
        try {
            this.downloadFile(imageUrl, new File("F:\\code\\images\\" + newName));
            data.put("image", newName);
            String json = MAPPER.writeValueAsString(data);

            // 写入到F:\code\data.json文件中
            // 如果找不到write方法,说明你的FileUtils所在的common-ios版本太低, 注意下是否版本冲突了.
            FileUtils.write(new File("F:\\code\\data.json"), json + "\n", "UTF-8", true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 下载文件
     *
     * @param url  文件url
     * @param dest 目标目录
     * @throws Exception
     */
    public void downloadFile(String url, File dest) throws Exception {
        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse response =
                HttpClientBuilder.create().build().execute(httpGet);
        try {
            FileUtils.writeByteArrayToFile(dest,
                    IOUtils.toByteArray(response.getEntity().getContent()));
        } finally {
            response.close();
        }
    }
}

运行后, 进入目录, 可以看到data.json已经899kb了.

13.1.2 爬取的数据导入到ES中

然后我们设置ES的文档

PUT http://{ip}:9200/haoke
# 下面的注释不要复制过去哈, 会报错, 删掉注释后再发请求.

{
	"settings": {
		"index": {
			"number_of_shards": 6,
			"number_of_replicas": 1
		}
	},
	"mappings": {
		"house": {
			"dynamic": false, # dynamic 参数来控制字段的新增, false为不允许像js一样新增字段(写入正常,不支持查询)
			"properties": {
				"title": {
					"type": "text",
					"analyzer": "ik_max_word" # 选择中文分词器分词后索引
				},
				"image": {
					"type": "keyword",
					"index": false  # false是不允许被索引的列表
				},
				"orientation": {
					"type": "keyword",
					"index": false
				},
				"houseType": {
					"type": "keyword",
					"index": false
				},
				"rentMethod": {
					"type": "keyword",
					"index": false
				},
				"time": {
					"type": "keyword",
					"index": false
				},
				"rent": {
					"type": "keyword",
					"index": false
				},
				"floor": {
					"type": "keyword",
					"index": false
				}
			}
		}
	}
}

建好ES的文档后, 我们使用Junit批量导入数据

package cn.itcast.es.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils;
import org.apache.http.HttpHost;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.client.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.util.List;

/**
 * 低级别rest风格测试
 */
public class TestESREST {
    // object 转 json 的工具
    private static final ObjectMapper MAPPER = new ObjectMapper();
    // es的rest客户端类
    private RestClient restClient;

    /**
     * 连接ES客户端
     */
    @Before
    public void init() {
        // 这里支持分布式ES, 你可以直接用','分割.
        RestClientBuilder restClientBuilder = RestClient.builder(
                new HttpHost("134.175.110.184", 9200, "http"));
        restClientBuilder.setFailureListener(new RestClient.FailureListener() {
            @Override
            public void onFailure(Node node) {
                System.out.println("出错了 -> " + node);
            }
        });
        this.restClient = restClientBuilder.build();
    }

    @After
    public void after() throws IOException {
        restClient.close();
    }


    @Test
    public void tesBulk() throws Exception {
        Request request = new Request("POST", "/haoke/house/_bulk");
        List<String> lines = FileUtils.readLines(new File("F:\\code\\data.json"),
                "UTF-8");
        String createStr = "{\"index\": {\"_index\":\"haoke\",\"_type\":\"house\"}}";
        StringBuilder sb = new StringBuilder();
        int count = 0;
        for (String line : lines) {
            sb.append(createStr + "\n" + line + "\n");
            if (count >= 100) {
                request.setJsonEntity(sb.toString());
                Response response = this.restClient.performRequest(request);
                System.out.println("请求完成 -> " + response.getStatusLine());
                System.out.println(EntityUtils.toString(response.getEntity()));
                count = 0;
                sb = new StringBuilder();
            }
            count++;
        }
    }
}

测试下

POST http://{ip}:9200/haoke/house/_search

{
    "query": {
        "match": {
            "title": {
            "query": "上海"
            }
        }
    },
    "highlight": {
        "fields": {
            "title": {}
        }
    }
}

13.2 提供搜索接口

13.2.1 添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

13.2.2 导入配置

# 这个cluster-name是根据你在浏览器{ip}:9200的cluster-name确定的, 不一致的话会报错找不到
spring.data.elasticsearch.cluster-name={通过{ip}:9200查询后可知}
# 9200是RESTful端口,9300是API端口。
spring.data.elasticsearch.cluster-nodes={ip}:9300

13.2.3 完善java代码

package cn.itcast.haoke.dubbo.api.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "haoke", type = "house", createIndex = false)
public class HouseData {
    @Id
    private String id;
    private String title;
    private String rent;
    private String floor;
    private String image;
    private String orientation;
    private String houseType;
    private String rentMethod;
    private String time;
}
package cn.itcast.haoke.dubbo.api.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Set;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SearchResult {
    private Integer totalPage;
    private List<HouseData> list;
    private Set<String> hotWord;

    public SearchResult(Integer totalPage, List<HouseData> list) {
        this.totalPage = totalPage;
        this.list = list;
    }
}
package cn.itcast.haoke.dubbo.api.controller;

import cn.itcast.haoke.dubbo.api.service.SearchService;
import cn.itcast.haoke.dubbo.api.vo.SearchResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Set;

@RequestMapping("search")
@RestController
@CrossOrigin
public class SearchController {
    @Autowired
    private SearchService searchService;
    @Autowired
    private RedisTemplate redisTemplate;
    private static final Logger LOGGER = LoggerFactory.getLogger(SearchController.class);

    @GetMapping
    public SearchResult search(@RequestParam("keyWord") String keyWord,
                               @RequestParam(value = "page", defaultValue = "1")
                                       Integer page) {
        if (page > 100) { // 防止爬虫抓取过多的数据
            page = 1;
        }
        SearchResult search = this.searchService.search(keyWord, page);
        
        Integer count = ((Math.max(search.getTotalPage(), 1) - 1) *
                SearchService.ROWS) + search.getList().size();
        // 记录日志
        LOGGER.info("[Search]搜索关键字为:" + keyWord + ",结果数量为:" + count);
        return search;
    }
}
package cn.itcast.haoke.dubbo.api.service;

import cn.itcast.haoke.dubbo.api.vo.HouseData;
import cn.itcast.haoke.dubbo.api.vo.SearchResult;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.stereotype.Service;

@Service
public class SearchService {
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;
    public static final Integer ROWS = 10;
    public SearchResult search(String keyWord, Integer page) {
        //设置分页参数
        PageRequest pageRequest = PageRequest.of(page - 1, ROWS);
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchQuery("title",
                        keyWord).operator(Operator.AND)) // match查询
                .withPageable(pageRequest)
                .withHighlightFields(new HighlightBuilder.Field("title")) // 设置高亮
                .build();
        // 查询内容
        AggregatedPage<HouseData> housePage = this.elasticsearchTemplate.queryForPage(searchQuery, HouseData.class);
        return new SearchResult(housePage.getTotalPages(), housePage.getContent());
    }
}

启动,发现报错:整合了Redis后,引发了netty的冲突, 解决方案如下:

package cn.itcast.haoke.dubbo.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DubboApiApplication {

    public static void main(String[] args) {
        // 指定即可
        System.setProperty("es.set.netty.runtime.available.processors","false");
        SpringApplication.run(DubboApiApplication.class, args);
    }
}

测试

http://127.0.0.1:18080/search?keyWord=上海&page=2

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/178636.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

还在纠结选择用什么浏览器?手机端用国产浏览器也很香

一说到受欢迎的电脑浏览器&#xff0c;大家肯定不约而同地说谷歌浏览器。微软edge浏览器能够同步书签、插件也非常多&#xff0c;因为这些优势深受国人的喜爱。有人纠结在国内选择谷歌好&#xff0c;还是edge浏览器好呢&#xff1f;可能有的人哪个也不选&#xff0c;反而在电脑…

Docker 解决 `denied: requested access to the resource is denied`

背景 由于不可描述的原因&#xff0c;相对于以前&#xff0c;最近在更加频繁的迁移服务器&#xff0c;简单的 Shell 脚本已经不能满足需求了&#xff0c;于是将所有的项目 Docker 化。 部分不含敏感配置的项目准备放到 DockerHub 上面&#xff0c;但是在 docker push 的时候报…

利用 Algolia 为静态博客搭建实现内容搜索

现在静态博客的标配之一就是博客搜索&#x1f50d;&#xff0c;我也是通过搭建博客发现了它&#xff0c;这篇主要记录一下怎么使用 algolia 完成博客搜索&#xff0c;自己的博客搭建使用的是 docusaurus 。 注册账号 首先需要去 algolia 官网注册自己的账号&#xff0c;可以直…

Java线程池(超详细)

1、基本概念 Java线程需要经过线程的创建&#xff0c;调用和销毁整个过程&#xff0c;频繁的创建和销毁会大大影响性能&#xff0c;所以引入的线程池&#xff1a; 好处&#xff1a; 提升性能&#xff1a;线程池能独立负责线程的创建、维护和分配线程管理&#xff1a;每个Java…

k8s安装kuboard面板

前面介绍了k8s的dashboard面板&#xff0c;这里介绍国人开发的kuboard面板&#xff0c;相较于dashboard面板&#xff0c;kuboard面板对很多运维调试功能做了很多增强。官方文档&#xff1a;https://www.kuboard.cn/install/v3/install.html#kuboard-v3-x-%E7%89%88%E6%9C%AC%E8…

实现一个TCP客户端——服务端协议

目录 TCP客户端常见的API&#xff1a; ServerSocket: Socket&#xff1a; TCP服务端(单线程版本) 属性构造方法: 启动服务端的start()方法 步骤一&#xff1a;接收客户端发送的socket 步骤二&#xff1a; 调用processConnection方法来处理客户端发送的连接 ①通过参数传入的…

影像组学|特征定义以及提取

一、 影像组学特征分类 1.1 影像组学特征分类 1.1.1 一阶统计特征 一阶统计特征&#xff0c;反应所测体素的对称性、均匀性以及局部强度分布变化。包括中值&#xff0c;平均值&#xff0c;最小值&#xff0c;最大值&#xff0c;标准差&#xff0c;偏度&#xff0c;峰度等。 …

【Linux】六、Linux 基础IO(三)|文件系统|软硬链接|文件的三个时间

目录 八、文件系统 8.1 磁盘 8.1.1 磁盘的物理结构 8.1.2 磁盘的存储结构 8.1.3 磁盘的逻辑结构 8.2 inode 九、软硬链接 9.1 软链接 9.2 硬链接 9.3 当前路径(.)和上级路径(..) 十、文件的三个时间 八、文件系统 上面的内容谈论的都是一个被打开文件&#xff0c;那…

如何将两个录音合成一个?这篇文章告诉你

现如今&#xff0c;很多小伙伴都加入到短视频行业当中。而短视频的制作往往需要将多段音频进行一个合并。那么问题来了&#xff0c;当你想多个音频进行合并在一起的时候&#xff0c;你是怎么做的呢&#xff1f;其实很简单&#xff0c;我们只需要借助市面上的一些合并软件就好了…

初始网络

文章目录初始网络局域网 / 广域网IP地址 和 端口号认识协议协议分层初始网络 这里可以先自行在网上了解一下网络的发展史 也就是互联网是怎么来的. 局域网 / 广域网 关于网络的发展史 , 会涉及到两个非常重要的术语 &#xff0c;也就是 局域网&#xff0c;和广域网 。 局域网 &…

JavaEE多线程-阻塞队列

目录一、认识阻塞队列1.1 什么是阻塞队列&#xff1f;1.2 生产者消费者模型1.3 标准库中的阻塞队列类二、循环队列实现简单阻塞队列2.1 实现循环队列2.2 阻塞队列实现一、认识阻塞队列 1.1 什么是阻塞队列&#xff1f; 阻塞队列&#xff1a;从名字可以看出&#xff0c;他也是…

简明Java讲义 2:数据类型和运算符

目录 1、安装IDE编辑器 2、关键字和保留字 3、标识符 4、分隔符 5、数据类型 6、基本类型的数据类型转换 7、表达式类型的自动提升 8、变量 9、运算符 10、运算符的优先级 1、安装IDE编辑器 在开始内容之前&#xff0c;先下载IDE&#xff0c;可以是Eclipse或STS&…

Python函数(函数定义、函数调用)用法详解

Python 中函数的应用非常广泛&#xff0c;前面章节中我们已经接触过多个函数&#xff0c;比如 input() 、print()、range()、len() 函数等等&#xff0c;这些都是 Python 的内置函数&#xff0c;可以直接使用。除了可以直接使用的内置函数外&#xff0c;Python 还支持自定义函数…

LeetCode刷题模版:201 - 210

目录 简介201. 数字范围按位与202. 快乐数203. 移除链表元素204. 计数质数205. 同构字符串206. 反转链表207. 课程表【未实现】208. 实现 Trie (前缀树)209. 长度最小的子数组210. 课程表 II【未实现】结语简介 Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您…

LeetCode[1319]连通网络的操作次数

难度&#xff1a;中等题目&#xff1a;用以太网线缆将 n台计算机连接成一个网络&#xff0c;计算机的编号从 0到 n-1。线缆用 connections表示&#xff0c;其中 connections[i] [a, b]连接了计算机 a和 b。网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其…

(十六)异步编程

CompletableFuture在Java8中推出&#xff0c;Java8中的异步编程就是依靠此类。几种任务接口四种任务无参数有一个参数有两个参数无返回值RunnableConsumerBiConsumer有返回值SupplierFunctionBiFunctionCompletionStage接口这个类中定义的许多能够链式调用的方法和组合方法时是…

Unity3DVR开发—— XRInteractionToolkit(PicoNeo3)

目录 一、开发前的准备 二、基础配置 三、Pico项目配置 四、添加基础功能 一、开发前的准备 1、为了方便开发&#xff0c;先在Pico开发者平台里下载预览工具 Pico开发者平台https://developer-global.pico-interactive.com/sdk?deviceId1&platformId1&itemId17 2、…

【哈希表】关于哈希表,你该了解这些!

【哈希表】理论基础1 哈希表2 哈希函数3 哈希碰撞3.1 拉链法3.2 线性探测法4 常见的三种哈希结构5 总结1 哈希表 哈希表 Hash table &#xff08;一些书籍翻译为散列表&#xff09; 哈希表是根据关键码的值而直接进行访问的数据结构。 直白来讲其实数组就是一张哈希表。 哈希表…

用1行Python代码识别增值税发票,YYDS

大家好&#xff0c;这里是程序员晚枫。 录入发票是一件繁琐的工作&#xff0c;如果可以自动识别并且录入系统&#xff0c;那可真是太好了。 今天我们就来学习一下&#xff0c;如何自动识别增值税发票并且录入系统~ 识别发票 识别发票的代码最简单&#xff0c;只需要1行代码…

CSS的总结

从HTML被发明开始&#xff0c;样式就以各种形式存在。不同的浏览器结合它们各自的样式语言为用户提供页面效果的控制。最初的HTML只包含很少的显示属性。 随着HTML的成长&#xff0c;为了满足页面设计者的要求&#xff0c;HTML添加了很多显示功能。但是随着这些功能的增加&…