Spring Boot集成WebMagic爬取京东商品信息

爬取分析

爬取https://search.jd.com/上的商品信息。只爬取笔记本电脑相关的商品信息。

商品列表

访问页面并搜索笔记本电脑,得到商品列表。 在这里插入图片描述

商品列表由id=J_goodsList的div包裹,每个商品对应一个li标签,li标签中clas=p-name的元素包含了商品详情页地址

在这里插入图片描述

点击分页,得到请求地址:https://search.jd.com/Search?keyword=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&wq=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&pvid=19a729aac00f4decbacb5de334b3be4e&page=3&s=56&click=0。分页规则:分页是以奇数分页,如:page=1、page=3、page=5

商品详情页

进入商品详情页分析需要的数据。例如:商品sku、商品标题、商品图片、商品价格、商品详情地址、商品店铺等

在这里插入图片描述

class=preview对应div下的img元素的data-origin属性获取图片地址。

在这里插入图片描述

标题的获取

在这里插入图片描述

规格的获取,通过该规格对应的sku,通过接口请求获取商品价格

在这里插入图片描述

执行流程

graph LR
A[解析商品列表页] ----> B[获取商品详情页]
B ----> C(解析页面获取数据)

搭建基础环境

添加依赖

        <!--SpringMVC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--SpringData Jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--MySQL连接包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <!--WebMagic核心包-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.5</version>
        </dependency>
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.5</version>
        </dependency>
        <!--WebMagic对布隆过滤器的支持-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.1-jre</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

POJO

@Entity
@Table(name = "item")
@Data
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    //商品标题
    private String title;
    //商品价格
    private Double price;
    //商品图片
    private String pic;
    //商品详情地址
    private String url;
    //规格
    private String standard;
    //店铺;
    private String shop;
    //创建时间
    private Date created;
}

Dao

public interface ItemDao extends JpaRepository<Item,Long> {
}

Service

public interface ItemService {
    void save(Item item);
}


@Service
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ItemDao itemDao;

    @Override
    @Transactional
    public void save(Item item) {
        this.itemDao.save(item);
    }
}

开启定时任务

在启动类开启定时任务开关,通过启动项目自动执行爬虫任务。

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

HttpUtils

@Component
public class HttpUtils {

    private PoolingHttpClientConnectionManager cm;

    public HttpUtils() {
        this.cm = new PoolingHttpClientConnectionManager();
        //设置最大连接数
        this.cm.setMaxTotal(100);
        //设置每个主机的最大连接数
        this.cm.setDefaultMaxPerRoute(10);
    }

    /**
     * 根据请求地址下载页面数据
     *
     * @param url
     * @return 页面数据
     */
    public String doGetHtml(String url) {
        //获取HttpClient对象
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(this.cm).build();
        //创建httpGet请求对象,设置url地址
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36");
        //设置请求信息
        httpGet.setConfig(this.getConfig());
        CloseableHttpResponse response = null;
        try {
            //使用HttpClient发起请求,获取响应
            response = httpClient.execute(httpGet);
            //解析响应,返回结果
            if (response.getStatusLine().getStatusCode() == 200) {
                //判断响应体Entity是否不为空,如果不为空就可以使用EntityUtils
                if (response.getEntity() != null) {
                    String content = EntityUtils.toString(response.getEntity(), "utf8");
                    return content;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭response
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //返回空串
        return "";
    }

    /**
     * 设置请求信息
     *
     * @return
     */
    private RequestConfig getConfig() {
        RequestConfig config = RequestConfig.custom()
                //创建连接的最长时间
                .setConnectTimeout(1000)
                // 获取连接的最长时间
                .setConnectionRequestTimeout(500)
                //数据传输的最长时间
                .setSocketTimeout(10000)
                .build();

        return config;
    }
}

自定义Pipeline保存结果

@Component
public class MyDataPipeline implements Pipeline {

    @Autowired
    private ItemService itemService;


    @Override
    public void process(ResultItems resultItems, Task task) {
        //获取封装好的商品详情对象
        Item itemInfo = resultItems.get("itemInfo");
        if (itemInfo != null) {
            this.itemService.save(itemInfo);
        }
    }
}

爬虫逻辑实现

@Component
public class JobProcessor implements PageProcessor {

    @Autowired
    private MyDataPipeline myDataPipeline;

    @Autowired
    private HttpUtils httpUtils;

    private  int currentPage=1;
    private String url ="https://search.jd.com/Search?keyword=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&wq=%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91&pvid=19a729aac00f4decbacb5de334b3be4e&s=56&click=0&page=";

    @Override
    public void process(Page page) {
        // 解析页面,获取商品信息详情的url地址
        Selectable css = page.getHtml().css("div#J_goodsList");
        List<Selectable> list = page.getHtml().css("div#J_goodsList li").nodes();
        if (list.size() == 0) {
            // 表示商品详情页,解析页面,获取商品详情信息,保存数据
            this.saveJobInfo(page);
        } else {
            // 表示商品列表页,解析出详情页的url地址,放到任务队列中
            for (Selectable selectable : list) {
                String url = selectable.css("div.p-name").links().toString();
                // 把获取到的url地址放到任务队列中
                page.addTargetRequest(url);
            }

            // 获取下一页的url
            currentPage=currentPage+2;
            // 把url放到任务队列中
            page.addTargetRequest(url+currentPage);
        }
    }

    //解析页面,获取商品详情信息,保存数据
    private void saveJobInfo(Page page) {
        // 创建商品详情对象
        Item item = new Item();
        // 解析页面
        Html html = page.getHtml();

        // 设置详情页地址
        String url = page.getUrl().toString();
        item.setUrl(url);

        // 商品图片地址
        String src = "https://"+html.css("div.preview img", "data-origin").toString();
        item.setPic(src);

        // 获取名称
        item.setTitle(html.css("div.sku-name","text").toString());

        // 获取当前选择的sku
        String sku = html.css("div#choose-attr-2 div.selected", "data-sku").toString();
        // 规格
        String standard = html.css("div#choose-attr-2 div.selected", "data-value").toString();
        item.setStandard(standard);

        // 商品价格,先获取sku,通过接口获取价格
        String jsonPrice = httpUtils.doGetHtml("https://" + "p.3.cn/prices/mgets?skuIds=J_" + sku);
        JSONObject priceObject = (JSONObject)JSONArray.parseArray(jsonPrice).get(0);
        item.setPrice(priceObject.getDouble("p"));


        // 店铺名称
        String text = html.css("div.popbox-inner a", "text").toString();
//        String text2 =  Jsoup.parse(html.css("div.popbox-inner a").toString()).text();
        item.setShop(text);
        item.setCreated(new Date());

        // 一个SPU商品里面包含多个SKU商品,每个SKU商品都有自己的价格,所以需要获取每个SKU商品的价格
        List<Selectable> nodes = html.css("div#choose-attr-2 div.item").nodes();
        for (Selectable node : nodes) {
            String nodeSku = node.css("div.item","data-sku").toString();
            // 排除上述处理的sku对应商品
            if (sku.equals(nodeSku)){
                continue;
            }
            page.addTargetRequest("https://item.jd.com/"+nodeSku+".html");
        }


        page.putField("itemInfo",item);
    }


    private Site site = Site.me()
            .setCharset("UTF-8")//设置编码
            .setTimeOut(10 * 1000)//设置超时时间
            .setRetrySleepTime(3000)//设置重试的间隔时间
            .setRetryTimes(3)//设置重试的次数
            .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36")//设置请求头
            ;
    @Override
    public Site getSite() {
        return site;
    }


    /**
     * 定时执行
     *
     * initialDelay:启动项目延迟执行
     * fixedDelay:每隔5s执行一次
     */
    @Scheduled(initialDelay = 2000, fixedDelay = 5000)
    public void process() {
        Spider.create(new JobProcessor())
                // 初始访问url地址
                .addUrl(url+currentPage)
                // 使用布隆过滤器
                .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))
                .thread(6)
                .addPipeline(this.myDataPipeline)
                .run();
    }
}

执行测试

启动项目,执行查看结果。

在这里插入图片描述