需求
爬取商品价格、销量、评论、收藏量、款式等基本数据以及详情图。
遇见的主要问题以及解决方式
对于一个从来没写过爬虫的人来说很多地方都是很困惑的。而且公司要求两三天就得出结果并用于生产,再加上自己轻微的代码洁癖,综合起来还是有些压力的。
所以也没时间去学习一些爬虫框架或者道友们的一些实现方案,回头来看,都是些常规操作。
写完之后,发觉这并不是一篇好文章,朋友们在这篇文章中可能获得不了太多有价值的信息
这篇文章其实不错
第一个问题
最开始抽取所有的商品URL,要爬取的网站的数据是Ajax动态加载的。所以直接代码获取链接数据,只会返回最初的静态页面。里面几乎不会有什么可用的信息。
你可以下载一个”toggle JavaScript”的插件,启动后点击该插件,可以看到哪些显式是通过JS异步加载过来的(是点击该插件,而不是仅仅启动就可以)
比如,未使用该插件时:
点击该插件过后,会发现很多数据没有加载出来,这里加载出来的都是最初的静态页面。往往都是“item.html“
要解决这个问题,主要得找到是哪个js脚本返回的数据,找到后用jousp解析html。我们需要一个个的去查看,看是哪个地方返回了我们需要的数据
数据往往都是以JSON的形式返回的,找到该入口后,接下来就是解决翻页。
我这里是直接在URL尾部添加&pageNo=,是可行的。
Document doc = null;
try {
// 解决UnsupportedMimeTypeException:ignoreContentType(true)
doc = Jsoup.connect(entry.getValue()).headers(SpiderHeader.headers).ignoreContentType(true).timeout(30000).get();
} catch (IOException e) {
e.printStackTrace();
}
后来发现请求淘宝移动端API可以基本满足我的需求,故没有继续慢慢的踩Ajax异步加载的坑
API中可能会有近十个参数,但是很多参数是可以去掉的。
还一种解决方案是采用第三方工具,模拟人的浏览器点击行为来使得数据加载。比如:Selenium、PhantomJs,但是这种方式感觉更繁琐,大家说效率也更低,甚至有可能在浏览器中开大量网页。没试过这种方式。
获取所有的商品URL后,解析JSON的问题
这里是自己挖的一个小坑,用jousp获取网页数据解析html是没有问题的,但是用jousp获取网页数据再用Jackson解析Json是会出现问题的。
因为这个Jsoup在返回的数据不符合HTML规范时,会构造<html><body>
等标签,导致JackSon解析出错。
改用java.net.URL
获取数据:
String jsonText;
StringBuilder sb = null;
InputStream is = new URL(entry.getValue()).openStream();
System.out.println("接口地址:" + entry.getValue());
try {
BufferedReader brd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));
sb = new StringBuilder();
int cp;
while ((cp = brd.read()) != -1) {
sb.append((char) cp);
}
} catch (IOException e) {
e.printStackTrace();
}
jsonText = sb.toString();
如果数据量较大,这里的StringBuilder的read()方法的效率可能会比较低,可改用read(buff)、readLine()等方法
Jsoup解析HTML较常用的方法:
- getElementsByTag定位到所有指定的标签
- text获得该Element下所有文字,空格隔开
- attr定位当前标签下的属性
Elements links = doc.getElementsByTag("dl");
System.out.println("获得dl标签个数:" + links.size());
for (Element e : links) {
// 去除ID中非法字符
String regex = "[`~!@#$%^&*()+=|{}':\";,\\\\.<>/?!¥…()—【】‘;:”“’。,、?]";
String id = e.attr("data-id").replaceAll(regex, "");
System.out.println("新数据" + e.text());
hashMap.put(id, e.text());
}
入库的问题
这个已经不属于爬虫范畴了,为了“省事儿“以及方便后期的扩展,便使用Mybatis操作数据库,可是一个没有错误堆栈信息的NullPointerException异常搞得我Debug半天都没找准问题根源,时间紧迫,还是手撸了一遍JDBC。
访问速度问题
访问太频繁会被反爬,设置同一sleep时间,也会被反爬。
设置一随机sleep时间即可。
比如:
// random sleep
Random rand = new Random();
Thread.sleep(rand.nextInt(3000) + 500);
下载图片
DataInputStream dis = new DataInputStream(url.openStream());
String newImageName = "/Users/wanghai/IdeaProjects/imgs/" + entry.getKey() + "_" + nameFlag + ".jpg";
// new imgFile
FileOutputStream fos = new FileOutputStream(new File(newImageName));
byte[] buffer = new byte[1024];
int length;
// write img data
while ((length = dis.read(buffer)) > 0) {
fos.write(buffer, 0, length);
}
dis.close();
fos.close();
其他问题
剩下就是一些常规操作和打日志了。
其实过程中还有犯不少错,但是没有及时的记录,现在也想不起具体的问题了。大都是一些判断无效链接、判断返回的JSON数据哪些无效,以及一些边界问题。
所以这就体现出单元测试的重要性了,个人感觉,对于需要进行很多的过滤、判断、匹配操作的时候,除了在理清思路、想出一些基本的边界问题之后再码代码之外,在每一个阶段性功能实现后就应该进行跟进测试,不然当写完所有逻辑之后,出现运行不报error但是结果却不是你想要的结果的情况时,排查起来还是比较麻烦的。
剩下的思路,就是获得所有需要请求接口的链接等信息,不使用多线程的话,封装到HashMap就行。请求接口后将需要数据解析出来封装到对象中,将对象置于ArrayList< ? extends SomeClass>,遍历arrayList,拿出每个对象,拼接添加sql语句,完成后,batch批量插入。以避免每一个对象插入一次,大量的数据库建立链接以及销毁链接的开销。
写在末尾的感慨:
好久没有做大数据相关的事情了,久了不碰,真的容易忘,时间时间时间啊。
2018-07-18增:
接口来的图片还是太少,公司要求要详情页所有图片,于是:
这就是硬爬图片了,在详情页中,查看任意以图片的连接地址,然后在F12中搜索关键词
双击搜索结果,很有可能就会得到你想要的数据返回链接,在该链接中,有详情页所有图片。
但是该链接,貌似无法拼凑,我尝试删减任意一参数,或者替换itemId,都是拒绝访问。但是考虑到我这商品数量并不多,两百来个,就化了2小时自己录入了链接。
接下来就是用Jsoup解析html(Jsoup会对返回的数据添加等表头,【如果没有的话】),然后使用select,对class、属性、标签进行选择,筛选自己需要的数据。
// 解析html,获得商品url
String regex = "^https://img.alicdn.com/imgextra/i.*";
Pattern p = Pattern.compile(regex);
StringBuilder stringBuilder = new StringBuilder();
Elements elements = doc.select("div[data-title=\"模特效果图\"]");
System.out.println("详情图elements size 期望是1,实际上是:" + elements.size());
for (Element e : elements) {
Elements elements2 = e.getElementsByTag("img");
for (Element e2 : elements2) {
String contents = e2.attr("src");
Matcher m = p.matcher(contents);
while (m.find()) {
stringBuilder.append(contents.substring(m.start(), m.end()));
stringBuilder.append(",");
}
}
}