京东商城商品数据批量抓取数据爬虫Node

前言

标题只是献给搜索引擎,增加搜索命中率

1. 任意进入一个京东商城的一个商品页(移动端)

https://item.m.jd.com/product/65812346102.html

2. 在network中查看每一条请求,并记录下有用的接口

https://wqcoss.jd.com/mcoss/reclike/getrecinfo?recpos=6159&pc=30&sku=100002937499&c1=670&c2=686&c3=693&callback=cb902007A&t=0.5218839571890463
获取sku列表

https://pe.3.cn/prices/mgets?source=wxsq&skuids=100002937499&callback=jsonp649215
获取3个价格


https://yx.3.cn/service/info.action?ids=100002937499&callback=jsonp510206
获取商品详情

//m.360buyimg.com/mobilecms/s750x750_

* spec: "标准版"imagePath: "jfs/t1/106198/2/12609/154308/5e4a1543E914ba663/5bf7b5904df6e816.jpg"color: "旗舰版 典雅黑"size: "2TB"name: "希捷(Seagate)2TB USB3.0移动硬盘 新睿品铭系列 2.5英寸 (轻薄小巧 自动备份 金属拉丝) 典雅黑"
https://wq.jd.com/bases/couponsoa/avlCoupon?callback=getCouponListCBA&cid=693&popId=8888&sku=100002937499&price=459.00&platform=4&t=0.22934273901457525
获取优惠券信息

//m.360buyimg.com/mobilecms/jfs/t1/98259/35/13245/303543/5e552c66E8b59a5e3/98adfedbb49899ae.jpg,/sku/jfs/

//m.360buyimg.com/mobilecms/sku/jfs/t1/98259/35/13245/303543/5e552c66E8b59a5e3/98adfedbb49899ae.jpg,/sku/jfs/t1/96467/9/13218/237657/5e552c66E75003205/96aedae67db48199.jpg,/sku/jfs/t1/85589/25/13237/182722/5e552c67E386e8342/590433c719412add.jpg,/sku/jfs/t1/91295/26/13301/299292/5e552c67E7f85e9a0/7547515530c09047.jpg

3. 经仔细查看,确认没办法直接通过一个或多个接口获取完整的商品数据

所以决定采用cheerio,从html页面中提取相关数据

4. 提取过程中发现以下几个问题

  1. 顶部轮播图片,只有首张链接是正常,其他几张要滑动后,才加载正确链接
  2. 商品名称,正常获取
  3. 商品价格,可以通过步骤2中找到的接口获取
https://pe.3.cn/prices/mgets?source=wxsq&skuids=100002937499&callback=jsonp649215

更换skuId,可以得到原价,最高价,最低价

  1. 商品详情内容图片,要下拉滑动,才会加载,否则没有详情内容图片
  2. 没有商品不同SKU规格数据以及不同规格的价格数据

5. 误区:采用了Puppeteer来实现滑动,来加载图片,获取图片链接

虽然采用Puppeteer,实现了我要的结果,但有点把简单的事情复杂化了。并且不好控制下拉的时间和速度,以及等待加载的时间。

6. 在html中找到了一段我要的数据

在html中搜索 `window._itemOnly`,然后会得到商品spu信息和sku多规格数据,同时解决了轮播图、内容详情图的问题

7. 已经能成功提取1个商品的完整数据,那如何批量提取商品数据?

思路:

  1. 研究商品skuId规律,然后遍历
  2. 通过分页,然后得到skuId列表,然后再列表中遍历

经过尝试,直接id递增获取商品数据,会存在大量不存在的商品。所以决定采用通过分页获取数据。

先在首页下拉加载,清空network,会发现分页接口

https://wqcoss.jd.com/mcoss/reclike/getrecinfo?pi=1&pc=22&recpos=6163&hi=%7Bpage%3A4%2Cpagesize%3A22%7D&_=1582986351823&callback=Zepto1582986301678

其中pi就是页数,一次能得到20多条sku数据

8. 已经能批量获取商品信息,如何保存?

批量获取后,直接通过js拼接出sql,并生成sql文件。
刚开始也有个误区,先获取了100页数据,然后再一次性写成sql。
后来改成了,获取一条商品信息,增量加到sql尾部。

9. 获取过的skuId不再重复获取

把获取过的skuId,增量写到文件中,然后每次请求前先判断,是否已经获取过。如果已经获取过,则不再重复获取

,40253760593,62694890335,65451538165,...

10. 下载所有图片

我并没有每获取一个商品信息,就把图片下载下来。先把图片链接生成批量保存到js文件中。

11. 批量下载图片

使用node异步批量下载图片,此时遇到了一个问题,一次性请求太多,请求报失败。
解决办法,设置使用setInterval设置时间间隔,首次每3秒。下载一遍肯定不够,那就再下载一遍(注意要判断,并跳过已经下载的)。

12. 代码如下

代码不可以直接执行,但是照着改改,肯定能得到你要的。

const fs = require('fs')
const cheerio = require('cheerio')
const axios = require('axios');
const path = require('path');
const method = require('./method');
var root = path.join(__dirname)

// 初始化方法
const  start = async () => {
    // getListData('63303803312')
    doFetch()
}

async function doFetch() {
    for (let i = 3; i < 100; i++) {
        console.log("******************   第" +i+ "页    ******************")
        let headers = {
            Referer: 'https://m.jd.com/',
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.19 Safari/537.36"
        }
       let page =  await axios({
            method: 'get',
            url: "https://wqcoss.jd.com/mcoss/reclike/getrecinfo?pi="+i+"&pc=22&recpos=6163&hi=%7Bpage%3A4%2Cpagesize%3A22%7D&_=1582986351823&callback=Zepto1582986301678",
            headers
        })

        let res = page.data.split('"data":')[1].split('"flow":')[0].trim()
        let skus = JSON.parse(res.substring(0, res.length-1))
        skus.forEach((item=>{
            let spuIds = fs.readFileSync(root + '/spu.txt','utf-8')
            if(spuIds.indexOf(','+item.sku+',')===-1){
                fs.appendFile(root + '/spu.txt', item.sku+',','utf8',function(error){
                    if(error){console.log(error);return false;}
                })
                getListData(item.sku)
            }else{
                console.log('跳过'+item.sku)
            }
        }))
    }
}


/* 定义函数 */
let getListData = async function(skuId) {
    try{
        let spuSql = ''
        let skuSql = ''
        let imageList = []

        let url = 'https://item.m.jd.com/product/'+skuId+'.html'
        //打开页面
        let page = await axios.get(url)
        let $ = cheerio.load(page.data)

        let res = page.data.split('window._itemOnly =  (')[1].split('window._isLogin')[0].trim()
        let spu = JSON.parse(res.substring(0,res.length-2)).item

        let specName = []
        Object.keys(spu.saleProp).forEach((key)=>{
            if(spu.saleProp[key]){
                specName.push(spu.saleProp[key])
            }
        })

        let valueList = [ {} ]
        Object.keys(spu.saleProp).forEach((key) => {
            if(spu.saleProp[key]){
                const value = spu.salePropSeq[key]
                let temp = []
                value.forEach(item => {
                    valueList.forEach(each => {
                        let copy = { ...each }
                        copy[key] = item || '默认'
                        temp.push(copy)
                    })
                })
                valueList = temp
            }
        })

        let data = {}
        // TODO 把'改成`
        data.name = spu.pName
        console.log(skuId, data.name)
        let swipeImage = []
        spu.image.forEach((item, index)=>{
            let name = skuId+'/s'+index+'.jpg'
            imageList.push(['https://m.360buyimg.com/mobilecms/s750x750_'+item, './static/'+name])
            swipeImage.push('https://xxxxxx.com/yjd/jd-spu/'+name)
        })
        data.imageList = swipeImage.join(',')
        data.mainImage = swipeImage[0]

        let priceResult = await axios.get('https://pe.3.cn/prices/mgets?source=wxsq&skuids='+skuId)
        let price = priceResult.data[0]
        let html = await axios.get('https://wqsitem.jd.com/detail/'+skuId+'_d'+skuId+'_normal.html')

        data.originalPrice = price.m
        data.minPrice = price.p
        data.maxPrice = price.op

        $ = cheerio.load(html.data.split('"content":"')[1].split('","plus"')[0]);
        let contentImage = []
        $('img').each((index, item)=>{
            let name = skuId+'/c'+index+'.jpg'
            let src = $(item).attr('src')
            if(src){
                if(src.indexOf('\\')===0){
                    src = src.substr(2, src.length-4)
                }
                if(src.indexOf('http') === -1){
                    src = 'https:'+src
                }
                imageList.push([src, './static/'+name])
                contentImage.push('https://xxxxxx.com/yjd/jd-spu/'+name)
            }
        })
        data.content = contentImage.join(',')
        data.id = skuId
        data.feature = JSON.stringify({category: spu.category,brandId: spu.brandId, brandName: spu.brandName })

        // TODO costPrice会出现精度问题和负数问题
        let sku = {
            spuId: skuId,
            image: swipeImage[0],
            originalPrice: data.originalPrice,
            costPrice: data.minPrice,
            price: data.minPrice,
            stock: 10000,
            sales: 0,
            stockUnit: spu.saleUnit || '个',
            specNames: specName.join('@'),
            specValues: '',
            length: spu.length,
            height: spu.height,
            width: spu.width,
            weight: Number(spu.weight),
            isDefault: 1,
        }

        let skuList = []
        valueList.forEach((item,index)=>{
            sku.isDefault = index === 0 ? 1 : 0
            let values = []
            Object.keys(item).forEach(key=>{
                values.push(item[key])
            })
            sku.specValues = values.join('@')
            skuList.push({...sku})
        })

       
        spuSql = `(${data.id}, 17, 1, '${data.content}', '${data.name}', '${data.name}', '${data.mainImage}', '${data.imageList}', 1, ${data.originalPrice}, ${data.maxPrice}, ${data.minPrice}, '${data.feature}'),\n`
        fs.appendFile(root + '/spu.sql', spuSql,'utf8',function(error){
            if(error){console.log(error);return false;}
        })
       
        skuList.forEach((item)=>{
            skuSql += `(${item.spuId}, '${item.image}', ${item.originalPrice}, ${item.costPrice}, ${item.price}, ${item.stock}, 0, '${item.stockUnit}', '${item.specNames}', '${item.specValues}', ${item.length}, ${item.width}, ${item.height}, ${item.weight}, ${item.isDefault}),\n`
        })
        fs.appendFile(root + '/sku.sql', skuSql,'utf8',function(error){
            if(error){console.log(error);return false;}
        })

        imageListStr = JSON.stringify(imageList) + ',\n'
        fs.appendFile(root + '/imageList.js', imageListStr,'utf8',function(error){
            if(error){console.log(error);return false;}
        })
    }catch (e) {
        console.log(skuId, '出错了')
    }

}



/**
 * 同步递归创建路径
 *
 * @param  {string} dir   处理的路径
 * @param  {function} cb  回调函数
 */
var mkdir = function(dir, cb) {
    var pathinfo = path.parse(dir)
    if (!fs.existsSync(pathinfo.dir)) {
        mkdir(pathinfo.dir,function() {
            fs.mkdirSync(pathinfo.dir)
        })
    }
    cb && cb()
}


const downloadBook = async (path, url) => {
    if (!fs.existsSync(path)) {
        mkdir(path)
    }
    if (!fs.existsSync(path)) {
        await method.downloadImage(url, url, path)
    }
}

module.exports = {
    start
}

批量下载图片代码

const fs = require('fs'),
    method = require('./product/method'),
    path = require('path')

const picList = require('./product/imageList')

/**
 * 同步递归创建路径
 *
 * @param  {string} dir   处理的路径
 * @param  {function} cb  回调函数
 */
var mkdir = function(dir, cb) {
    var pathinfo = path.parse(dir)
    if (!fs.existsSync(pathinfo.dir)) {
        mkdir(pathinfo.dir,function() {
            fs.mkdirSync(pathinfo.dir)
        })
    }
    cb && cb()
}

// 下载书本
const downloadBook = async (path, url) => {
    if (!fs.existsSync(path)) {
        mkdir(path)
    }
    if (!fs.existsSync(path)) {
        if(url.indexOf('\\n') > 0){
            url =  url.substring(url, url.length - 2)
        }
        await method.downloadImage(url, url, path)
    }
}

let length = picList.length - 1
let i = 0
let handle = setInterval(()=>{
    for (let j = i*10; j < (i+1)*10; j++) {
        if(j>length){
            clearInterval(handle)
            console.log('结束了')
            break
        }
        picList[j].forEach((each,index)=>{
            downloadBook(each[1], each[0])
        })
    }
    i++
}, 1000)

下载图片的方法

  // 下载图片到本地
  async downloadImage (Referer, imageSrc, fileName) {
    let headers = {
      Referer: '360buyimg.com',
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.19 Safari/537.36"
    }
    await axios({
      method: 'get',
      url: imageSrc,
      responseType: 'stream',
      headers
    }).then(function(response) {
      console.log('.')
      response.data.pipe(fs.createWriteStream(fileName))
    }).catch((e)=>{
      console.log(e)
      console.log(fileName + ":" + imageSrc + "下载失败")
    })
  }

推荐阅读更多精彩内容

  • 2017年7月 1、签订劳动合同,转正,实习期结束 2、React-Native 学习研究 2017年6月19日星...
    阿星Plus阅读 1,948评论 1 10
  • 1.JAVA的垮平台原理 JVM也是一个软件,不同的平台有不同的版本。我们编写的Java源码,编译后会生成一种 ....
    Java高级架构阅读 3,268评论 0 93
  • 感赏孩子知道打开电脑做作业,感赏孩子用唱歌的方式排列心胸的郁闷,感赏孩子在学校英语口语能考20多分,感赏孩子按时上...
    爱瑶爱瑶爱瑶阅读 30评论 0 1
  • 1、白开水德国的最新研究发现,当在一定时间内喝下17盎司(约两杯)水后,人体内的新陈代谢会加快30%。按照这一研究...
    浅薄123阅读 10评论 0 1
  • 太阳,普照着万物大地 万物离不开太阳 姑娘,你温柔的看着我 我也离不开你的眼光 冬日的太阳在我头顶 那是一刻燃烧的...
    杨诗空阅读 31评论 0 0