上一篇博客 “爬虫让我再次在女同学面前长脸了~(现实版真实案例)” 说到了帮女同学批量下载试题,我把文章同步到了,竟然有41个赞 + 21个评论 + 155个收藏,难道大家和我的目的都一样:爬虫 liao mei ?
本篇文章主要介绍如何“快速”抓取淘宝商品信息,从几个维度统计并且进行可视化,本次案例使用关键字 “安踏篮球鞋男鞋”,(仅仅是因为我最近买了一双鞋子,然后就想到了这个商品而已)
没有故事,没有背景,就是突然想。。。
先来接地气几个词,一般来说爬虫可以分为 【通用爬虫 、垂直爬虫】,那么是如何定义的呢?
通用爬虫:通用爬虫不需要理会网站哪些资源是需要的,哪些是不需要的,一并抓取并将其文本部分做索引(比如:百度爬虫、搜狗爬虫。。。)垂直爬虫:垂直爬虫往往在某一领域具有其专注性,并且垂直爬虫往往只需要其中一部分具有垂直性的资源,所以垂直爬虫相比通用爬虫更加精确,这也是比较常见的爬虫。如果上面的解释还不够接地气的话,举个简单例子 小明想找一个女朋友,但是小明对女朋友要求为 null,只要 [ 是一个女的都行 ] 那种,这就可以理解为通用爬虫; 但是小明如果眼光比较挑,要求女朋友必须是 [ 身高165+,体重90,大眼睛,长头发,小蛮腰 ],这就可以理解为垂直爬虫。
淘宝爬虫,很多文章都在讲解淘宝登录,然后分析ua参数等,以前我也很执着去分析过,真的挺难,后面我就没有继续跟下去了,就使用浏览器驱动实现登录,如 selenium (需要修改参数,否则淘宝能识别) 或者 JxBrowser 等等。。。
除开淘宝登录来看,淘宝的底线还是比较低的,暂时没有很高防线,你携带 cookie 即可,没错就是携带“饼干”给淘宝,他就给你通行了,所以本篇博客是通过携带 cookie 进行快速获取到数据的,(个人:有时间就去学习逆向分析,但是真正实际爬的话,使用最低的成本获取最高的效益,巧劲最大化)
题外话,虽然说一般的大厂不轻易封IP (误伤太大),但如果真的想海量采集淘宝商品数据的话,还必须要花点时间研究反爬策略的,否则很难进行海量抓取,因为文章后面有可能是有点触发反爬了 (后面再说)
如果在电脑首次上淘宝的话,你想搜索商品是必须要求登录的,淘宝的要求登录就不是我上一篇文章(撩妹佳文)那种假登录限制了,而且整体接口都要求必须携带凭证(cookie)访问的了,这里介绍一个巧劲,因为如果使用登录的 cookie 访问淘宝的话,淘宝实际上是知道你是谁的,有你访问的记录的,然后你上淘宝可能会给你推送商品的,甚至对你账号进行限制 、反爬等,因此写一个爬虫没必要搭上那么大的风险吧,所以登录完成之后,就退出,这个时候浏览器还是可以正常搜索产品的,意思就是说不要使用登录的 cookie 进行爬取数据。。。
五个抓包步骤如下
观察 json 结构:复制上面的抓包的数据,然后进行 json 视图查看
如何分页?
上面的抓包只是一页的数据,那么如何抓取淘宝搜索其他页呢? 我可以直接告诉你的是,通过在 url 传递一个 s={最后一个商品的位置} 实现的分页…
好吧 我们来简单看一下吧:
在上面抓包中,我们并没有看到 url 传递了 s 这个参数然后我们先来点击一下 “下一页”, 多点几次观察几次,你就知道了 s 参数的变化了观察 s 变化,第二页s=44,第三页s=88,第四页s=132,…
首页没有 s 参数 第二页:https://s.taobao.com/search?q=%E5%AE%89%E8%B8%8F%E7%AF%AE%E7%90%83%E9%9E%8B%E7%94%B7%E9%9E%8B&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20200705&ie=utf8&bcoffset=3&ntoffset=3&p4ppushleft=1%2C48&s=44 第三页:https://s.taobao.com/search?q=%E5%AE%89%E8%B8%8F%E7%AF%AE%E7%90%83%E9%9E%8B%E7%94%B7%E9%9E%8B&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20200705&ie=utf8&bcoffset=0&ntoffset=6&p4ppushleft=1%2C48&s=88 第三页:https://s.taobao.com/search?q=%E5%AE%89%E8%B8%8F%E7%AF%AE%E7%90%83%E9%9E%8B%E7%94%B7%E9%9E%8B&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20200705&ie=utf8&bcoffset=0&ntoffset=6&p4ppushleft=1%2C48&s=132来到这里,可以去掉其他不必要的参数,结论是:https://s.taobao.com/search?q={url encode keywords}&s={开始位置}
经过上面繁琐的步骤,终于可以着手编码了,因为爬取数据之后需要进行可视化,所以数据肯定是需要持久化的,为了方便就保存到 csv 文件中。
逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。
在代码里注释得很清楚的了,这里直接贴代码出来即可
为了简洁,我贴出主要的代码,想要获取整体源码项目的话,关注微信公告号( it_loading 回复 “淘宝爬虫” 即可)
FastHttpClient.java
https://blog.csdn.net/JinglongSource/article/details/107136862
TaoBaoSpider1.java
import cn.shaines.spider.util.FastHttpClient; import cn.shaines.spider.util.FastHttpClient.Response; import cn.shaines.spider.util.FastHttpClient.ResponseHandler; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONPath; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpGet; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; /** * @author for.houyu@qq.com */ @Slf4j public class TaoBaoSpider1 { private static final String listUrlTemplate = "https://s.taobao.com/search?q=${keywords}&s=${index}"; private static String cookie = "enc=l5xju4OJMzTwwtBkVJWbvFPcQ%2B6n6%2FWdRaE4iECxQOQiEtA45RBOyXQu0gZbOcIVEO6oxrRWvtu6mgSv4JZa8w%3D%3D; thw=cn; hng=CN%7Czh-CN%7CCNY%7C156; sgcookie=ESMDjIxrMIKwi48qmS8xP; tfstk=cCOGBNOa3dW_xoWHFf16ovEkfpHRZKNVqIRBTCGS8wNPBsdFiO2UUAat-GxMM-1..; tracknick=; cna=8XsbF0X0cVkCAbcg0Pt/qWLy; t=bb72251ba9e04aa4ffe5119a746b1f35; v=0; cookie2=1d43fd087e0cf8b4268f2e8ddcd4aea0; _tb_token_=e30835115bdfe; alitrackid=www.taobao.com; lastalitrackid=www.taobao.com; JSESSIONID=07CB217DBB9367EF2E0CCF7AF29AA9A0; isg=BDo6WaueMbQrb7zN4rxs3mhmi2Bc677FZ6MxoUQ_y0y9N99xLH6N1UYBh8PrpzZd; l=eBMkD1V4QZHXKy_vBO5whurza77ONdAfCsPzaNbMiInca6ZFN1uJuNQqG5uBldtjgtfj7etrb3kJjRUpziUdg2HvCbKrCyCk6Yp6-"; private FastHttpClient httpClient; private BufferedWriter writer; private Set<String> filter; public static void main(String[] args) throws Exception { // 搜索关键字 final String keywords = "安踏篮球鞋男鞋"; // 每页44条数据 final int limit = 44; TaoBaoSpider1 spider = new TaoBaoSpider1(); spider.init(keywords); for (int page = 0; page <= 99; page++) { log.info("正在准备下载页数: {}", page + 1); String html = spider.getListHtml(keywords, page * limit); List<Goods> list = spider.parse(html); log.info("解析得到数量: [{}]", list.size()); if (list.isEmpty()) { break; } list = spider.doFilter(list); log.info("过滤后数量: [{}]", list.size()); list.forEach(v -> { List<String> row = Arrays.asList(v.getRaw_title(), v.getView_price(), v.getView_fee(), v.getNick(), v.getItem_loc(), v.getView_sales(), v.getPic_url(), v.getDetail_url(), v.getComment_url(), v.getShopLink(), "_" + v.getNid(), "_" + v.getPid()); spider.writeRow(row); }); // 睡眠3 ~ 10秒 Thread.sleep(ThreadLocalRandom.current().nextLong(3000, 10000)); log.info("\r\n"); } } private List<Goods> doFilter(List<Goods> list) { list = list.stream().filter(v -> !filter.contains(v.getNid())).collect(Collectors.toList()); filter.addAll(list.stream().map(Goods::getNid).collect(Collectors.toSet())); return list; } /** * 写入一行数据 * @param row 一行数据 */ protected void writeRow(List<String> row) { // 写入一行数据, csv的一行格式为,分割的, 但是这里使用","分割,主要就是为了统一作为字符串 // 如: // "姓名","年龄" // "张三","123" String line = row.stream().map(v -> v.replace("\"", "\"\"").replace(",", ",,")).collect(Collectors.joining("\",\"", "\"", "\"")); try { writer.write(line); writer.newLine(); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } /** * 解析html * @param html the html */ protected List<Goods> parse(String html) { if (StringUtils.isEmpty(html) || (html.contains("登录页面") && html.contains("全登陆不允许iframe嵌入"))) { throw new RuntimeException("获取列表HTML失败,请检查,如更新cookie等..."); } String script = Arrays.stream(StringUtils.substringsBetween(html, "<script", "</script>")) // 过滤包含 g_page_config 和 auctions 的 script 脚本 .filter(v -> v.contains("g_page_config") && v.contains("itemlist")) // 获取第一个符合条件的脚本(原则来上说这里只能返回一个, 否则说明上面的过滤不严谨) .findFirst() // 如果没有匹配的就抛异常, 说明解析页面失败 .orElseThrow(() -> new RuntimeException("解析页面失败,请检查更新代码")); // 观察 script 内部其实是一个json格式的字符串, 因此找到分割点进行切割字符串返回一个json串 String json = StringUtils.substringBetween(script, "g_page_config", "g_srp_init"); json = json.substring(json.indexOf("{"), json.lastIndexOf("}") + 1); // 使用 JSONPath 实现快速解析json, 这里的意思是查找n级 itemlist 下的 n级auctions (说明:alibaba fastjson 就有 JSONPath 的实现) Object eval = JSONPath.eval(JSON.parseObject(json), "$..itemlist..auctions"); if (!(eval instanceof List)) { throw new RuntimeException("解析JSON列表失败, 请检查更新代码"); } List<JSONObject> auctions = (List<JSONObject>) eval; // 转换为目标对象 Goods List<Goods> result = auctions.stream().map(v -> v.toJavaObject(Goods.class)).collect(Collectors.toList()); // result.forEach(System.out::println); return result; } /** * 根据关键字获取列表HTML * @param keywords 关键字 如: 安踏篮球鞋男鞋 * @param index 开始位置, 如0, 每页显示44条数据, 因此0, 44, 88, ... */ protected String getListHtml(String keywords, int index) { // 中文进行URL编码 keywords = FastHttpClient.encodeURLText(keywords, StandardCharsets.UTF_8); String url = listUrlTemplate.replace("${keywords}", keywords).replace("${index}", index + ""); // 创建一个GET请求 HttpGet httpGet = httpClient.buildGet(url); // 执行请求, 获取响应 Response<String> response = httpClient.execute(httpGet, ResponseHandler.ofString()); return response.getData(); } /** * 资源初始化 */ protected void init(String keywords) { // 初始化 httpClient 并且设置 cookie 每次请求都会携带cookie信息 httpClient = FastHttpClient.builder().setCookie(cookie).build(); filter = new HashSet<>(1024); try { // 初始化 CSV 文件呢 File file = Paths.get(System.getProperty("user.dir"), "temp", "spider", keywords + ".csv").toFile(); file.getParentFile().mkdirs(); writer = new BufferedWriter(new FileWriter(file, false)); // 准备header List<String> header = Arrays.asList("标题", "单价", "运费", "店名", "发货地址", "售量", "首页图", "明细地址", "评论地址", "购买地址", "nid", "pid"); this.writeRow(header); } catch (IOException e) { e.printStackTrace(); } } public static class Goods { /** nid */ private String nid; /** pid */ private String pid; /** 标题 */ private String raw_title; /** 首页图 */ private String pic_url; /** 明细地址 */ private String detail_url; /** 单价 */ private String view_price; /** 运费 */ private String view_fee; /** 发货地址 */ private String item_loc; /** 售量 */ private String view_sales; /** 店名 */ private String nick; /** 评论地址 */ private String comment_url; /** 购买地址 */ private String shopLink; public String getNid() { return nid; } public void setNid(String nid) { this.nid = nid; } public String getPid() { return pid; } public void setPid(String pid) { this.pid = pid; } public String getRaw_title() { return raw_title; } public void setRaw_title(String raw_title) { this.raw_title = raw_title; } public String getPic_url() { return pic_url; } public void setPic_url(String pic_url) { this.pic_url = pic_url; } public String getDetail_url() { return detail_url; } public void setDetail_url(String detail_url) { this.detail_url = detail_url; } public String getView_price() { return view_price; } public void setView_price(String view_price) { this.view_price = view_price; } public String getView_fee() { return view_fee; } public void setView_fee(String view_fee) { this.view_fee = view_fee; } public String getItem_loc() { return item_loc; } public void setItem_loc(String item_loc) { this.item_loc = item_loc; } public String getView_sales() { return view_sales; } public void setView_sales(String view_sales) { this.view_sales = view_sales; } public String getNick() { return nick; } public void setNick(String nick) { this.nick = nick; } public String getComment_url() { return comment_url; } public void setComment_url(String comment_url) { this.comment_url = comment_url; } public String getShopLink() { return shopLink; } public void setShopLink(String shopLink) { this.shopLink = shopLink; } @Override public String toString() { final StringBuilder sb = new StringBuilder("Goods{"); sb.append("nid='").append(nid).append('\''); sb.append(", pid='").append(pid).append('\''); sb.append(", raw_title='").append(raw_title).append('\''); sb.append(", pic_url='").append(pic_url).append('\''); sb.append(", detail_url='").append(detail_url).append('\''); sb.append(", view_price='").append(view_price).append('\''); sb.append(", view_fee='").append(view_fee).append('\''); sb.append(", item_loc='").append(item_loc).append('\''); sb.append(", view_sales='").append(view_sales).append('\''); sb.append(", nick='").append(nick).append('\''); sb.append(", comment_url='").append(comment_url).append('\''); sb.append(", shopLink='").append(shopLink).append('\''); sb.append('}'); return sb.toString(); } } }效果如下
文件保存路径默认在项目路径 $/temp/spider/xxx.csv
一共爬了2074条数据,爬到大概50页的时候,发现返回的数据都是重复的了(根据nid过滤) 大概猜测原因:
nid 字段不具备唯一性过滤淘宝反爬淘宝限制返回数量(虽然写着99页,实际真实客户也不可能是真的翻了99页找商品…)…以下可视化数据仅供学习参考,不具备任何依据判断,不承担任何责任。
1. 淘宝 - 安踏篮球鞋男鞋 - 发货地词云图
2. 淘宝 - 安踏篮球鞋男鞋 - 店铺售量冲浪榜
仅展示店铺售量1900+以上店铺,不上榜的店铺纳入 “其他”
3. 淘宝 - 安踏篮球鞋男鞋 - 价格&售量表1
4. 淘宝 - 安踏篮球鞋男鞋 - 价格&售量表2
上面的代码完成一个关键字的搜索,顶多可以算一个爬虫,并不能算一个完整的爬虫系统,一个爬虫系统应该具备以下的几个组件。(待完善)
获取源码,请关注我的公众号 :IT加载中(it_loading) 回复 "淘宝爬虫"即可
公众号:IT加载中(it_loading) :https://blog.csdn.net/JinglongSource 博客:https://shaines.cn/ 邮箱:for.houyu@qq.com
程序员 [ 后宇 ],是一个关注编程,热爱技术的Java后端开发者,热衷于 [ Java后端 ],[ 数据爬虫领域 ]。不定期分享 I T 技能和干货!!欢迎关注 “IT加载中”,一个只出 干货 和 实战 的公众号。
