作为一个java互联网金融开发从业者,一直对爬虫有很大的兴趣,之前也学过Python语言,但是拿是很久之前的事情了。 虽然Python语言语法不难,再要捡起来也需要一些时间。 就想着为什么不能用java个爬虫呢?java可是世界上第二好的语言。
说起来就动手。
一、准备阶段
二、豆瓣top250
下载源码后,根据文档跑了几个源码例子,webmagic简直简单清晰明了。 于是乎准备自己着手一个爬虫, 由于最近一直在看电影,所有挑了个软柿子捏一捏。 豆瓣top250 就开始了。
1、实体类(Movie)
这里我需要的信息不多,所以只有电影名字,电影简介及电影评分。
public class Movie {
private String name;
private String comment;
private double score;
}
2、编写MoviceService 关键得实现PageProcessor接口中的process方法和getSite方法
①. site
Site site = Site.me()
.setRetryTimes(3)
.setSleepTime(1000)
.setCharset("UTF-8")
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36");
setRetryTimes:爬虫重试间隔
setSleepTime:相当于Thread.sleep(1000)
addHeader:这个千万不能少,一般网页都是通过ua区别机器还是正常访问情况。
②. 最最关键的process核心方法
打开豆瓣250网站 豆瓣250熟练的F12打开控制台,Ctrl+Shift+C 快速定位到我需要的标签位置。这里我选用了Xpath选择器。快速有简单。
//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[1]/a/span[1]
紧接着出现了第一个问题, 我要的是这一页中所有的电影的数据。这就不得不找排列规律了。我开始了用肉眼盯着一个个标签对比,眼睛都快瞎了。突然的灵机一动,我copy出两个标签,对比一下那个地方不同不就可以了。
//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[1]/a/span[1]
//*[@id="content"]/div/div[1]/ol/li[2]/div/div[2]/div[1]/a/span[1]
经过对比,发现只有li标签不同。抽取代码如下:
List<String> m_names = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]/text()").all();
List<String> comments = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/ol/li/div/div[2]/div[2]/p[2]/span/text()").all();
List<String> scores = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/ol/li/div/div[2]/div[2]/div/span[2]/text()").all();
③ 翻页
这里豆瓣的翻页是get请求,并且是没有加密限制的。所以参数很容易找出来:
public static void main(String[] args) {
String url = "https://movie.douban.com/top250?start=";
for (int i = 0; i < 10; i++) {
Spider.create(new MovieService())
.addUrl(url + i * 25)
.thread(5)
.run();
}
}
④、入库
这里自己写了个preparedStatement,直接用原生jdbc插入的数据库
public class MovieDao {
private Connection connection;
private PreparedStatement preparedStatement;
public MovieDao(){
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://47.94.174.237/movie?useUnicode=true&characterEncoding=utf8";
String username = "我是用户名";
String password = "我是密码.";
connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void add(Movie movie){
try {
String sql = "insert into movie(m_name, comment, score) value(?,?,?)";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, movie.getName());
preparedStatement.setString(2, movie.getComment());
preparedStatement.setDouble(3,movie.getScore());
preparedStatement.execute();
System.out.println("插入成功<"+movie.getName()+"::"+movie.getComment());
} catch (SQLException e) {
System.out.println("插入失败");
e.printStackTrace();
}finally {
try {
if (connection != null){
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (preparedStatement != null){
preparedStatement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
⑤ 爬虫开始
⑥ 爬虫结束
三、 网易云音乐评论
在这我以为还能跟豆瓣一样简单,爬虫也不过如此。
1、数据来源
同样写了个site利用xpath选择器拿到标签地址,然后翻页找翻页参数。发现,不行啊,这是post请求... 所以失败了。查看源码,github上有人也提出了这类问题。
Request request = new Request(url);
request.setMethod(HttpConstant.Method.POST);
于是修改代码,还是不行???
一番谷歌百度后,原来是这两个参数在搞鬼。
2、解密
这里解密算法啥的我就不详细说了(其实是看的一知半解,说不出来),参见知乎 综合了几个人的代码后:
package com.cxm.util;
import org.springframework.util.Base64Utils;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
public class MusicEncrypt {
/***
* 密钥
*/
private static String sKey = "0CoJUm6Qyw8W8jud";
/**
* 偏移量
*/
private static String ivParameter = "0102030405060708";
private static String context = "{rid: \"R_SO_4_25641368\",offset: \"0\",total: \"true\",limit: \"20\",csrf_token: \"\"}";
/**
* aes加密
* @param content 加密内容
* @param sKey 偏移量
* @return
*/
public static String AESEncrypt(String content,String sKey) {
try {
byte[] encryptedBytes;
byte[] byteContent = content.getBytes("UTF-8");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(sKey.getBytes(), "AES");
IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
encryptedBytes = cipher.doFinal(byteContent);
return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
public static String rsaEncrypt() {
String secKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c";
return secKey;
}
/**
* @param songId 歌曲ID
* @param paging 是否第一页 true 第一页 其余传入false
* @param nowPageNum 当前页数
* @return
*/
public static String makeContent(String songId, String paging, int nowPageNum) {
int offset;
if (nowPageNum < 1) {
offset = 20;
}
offset = (nowPageNum - 1) * 20;
String baseContent = "{rid: \"R_SO_4_%s\",offset: \"%d\",total: \"%s\",limit: \"20\",csrf_token: \"\"}";
return String.format(baseContent, songId, offset, paging);
}
/**
* 获取评论的2个参数设置
*
* @param content
* @return
*/
public static Map<String, Object> makePostParam(String content) {
Map<String, Object> map = new HashMap<>();
map.put("params", MusicEncrypt.AESEncrypt((AESEncrypt(content, sKey)), "FFFFFFFFFFFFFFFF"));
map.put("encSecKey", MusicEncrypt.rsaEncrypt());
return map;
}
/**
* 直接调用此方法
* @param songId
* @param paging
* @param nowPageNum
* @return
*/
public static Map<String, Object> makePostParam(String songId, String paging, int nowPageNum) {
return makePostParam(makeContent(songId, paging, nowPageNum));
}
}
2、爬虫代码
这里是ajax返回的json数据。所以用的是JsonPathSelector选择器。
List<String> contentList = new JsonPathSelector("$.comments.[*].content").selectList(page.getRawText());
List<String> timeList = new JsonPathSelector("$.comments.[*].time").selectList(page.getRawText());
List<String> nicknameList = new JsonPathSelector("$.comments.[*].user.nickname").selectList(page.getRawText());
为了不被网易发现...
for (int i = 1; i <= 5930; i++) {
System.err.println("开始查询页数:" + i);
String url = "https://music.163.com/weapi/v1/resource/comments/R_SO_4_474567580?csrf_token=f6618e7d1b51e227d863ffc579a27c95";
Request request = new Request(url);
Map<String, Object> makePostParam = MusicEncrypt.makePostParam("474567580", "false", i);
request.setRequestBody(HttpRequestBody.form(makePostParam, "utf8"));
request.setMethod(HttpConstant.Method.POST);
Spider.create(new CommentService())
// .addUrl("https://music.163.com/song?id=474567580")
// .setDownloader(httpClientDownloader)
.addRequest(request)
.thread(3)
.run();
Thread.sleep((new Random().nextInt(1000) + 1000));
}
好吧, 还是被发现了,封了我一天的ip... 但是这不可能阻止我学习欲望。
加入代理ip,为此我还专门去爬取了代理ip网站的ip。真是差火,没一个能用的。白嫖是不可能的了。 买来ip后,继续爬取。
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy(ProxyHost,ProxyPort,ProxyUser,ProxyPass)));
Spider.create(new CommentService())
.setDownloader(httpClientDownloader);
我的天, 看着一条条数据写入磁盘,别提心里多舒服了。
2、结果
在我的代码跑了一个小时后, 发现,已经拿不到数据了。 翻看日记发现,从250页开始后面对数据都没有了。一度怀疑是ip封了,回家后马上换网络继续爬取。 还是同一个问题,250页之后都没了。于是我开始倒着爬试试,从最后一页往前爬,发现,同样,只能爬取最后250页数据。 这是为什么?
换ip,换歌,能换的都换了。 最后手动翻页到250页,发现,wdnmd,第250页之后到倒数250页之间的数据是不展示的,不展示我咋拿...
简直了,这是一道无解题
网易,你厂的程序员还真会玩...
最后,特别不甘心,琢磨了两天,居然是这样的一个结果,如果谁有其他的办法,请一定告诉我。