分布式爬虫框架实践

配置环境

本项目基于spring boot框架快速开发,相比于spring mvc,提供了很多开箱即用的模块,即spring-boot-starter.

之所以能做到开箱即用,自动配置,是因为提供了很多自动化配置的类,这些自动化配置的类会判断classpath中是否存在自己需要的那个类。

因为在代码实现中,IP的抓取和校验都需要一定的时间,故需要一个缓存机制来存储。

增加lombok工具

使用 @Slf4j 注解打印日志更加方便。

Maven无法自动补全依赖库

  • 将下载好的spring的jar包放在./m2/responsity中,之后在maven的setting中手动更新本地仓库的jar索引来解决。
  • 打开设置界面,选中本地的仓库,点击右上角的update,更新maven仓库索引。 这样对于已经下载到本地的jar都可以自动进行补全了。
  • 缺少resource文件夹,设置成resource boot,即mark dectionary as source boot,可以在该文件夹下面建立包。
  • 在pom.xml文件中直接添加依赖,然后点击右上角搜索框,输入maven project,点击刷新,可以对依赖库进行自动下载。

CacheService缓存类

提供缓存服务,用于缓存代理数据。

要想使爬虫健壮,则需要用到代理池,每次请求的时候都带上代理去请求,隐藏爬虫身份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Service
public class CacheService<K, V> {
private static final Logger LOGGER = LoggerFactory.getLogger(CacheService.class);

private int cacheMaxSize = 3000;

public void setCacheMaxSize(int cacheMaxSize) {
this.cacheMaxSize = cacheMaxSize;
}

// 缓存
private Cache cache = CacheBuilder.newBuilder()
.maximumSize(cacheMaxSize)
.removalListener(new RemovalListener() { //移出缓存时执行的操作
@Override
public void onRemoval(RemovalNotification notification) {
if (notification.wasEvicted()) {
log.info("key[\" + notification.getKey() + \"] was removed with expired.");
} else {
log.info("key[\" + notification.getKey() + \"] was updated with put operation.");
}
}
})
.build();
1
2
3
4
5
6
7
8
9
10
/**
* The entry's expiration timestamp has passed. This can occur when using
* {@link CacheBuilder#expireAfterWrite} or {@link CacheBuilder#expireAfterAccess}.
*/
EXPIRED {
@Override
boolean wasEvicted() {
return true;
}
},

RemovalNotification中包含了缓存的key、value以及被移除的原因RemovalCause。通过源码可以看出,移除原因与容量管理方式是相对应的。如果时间过期,则打印日志。

1
2
3
4
cache.getIfPresent(key);
cache.invalidate(key);
cache.asMap();
cache.put(key, value);

UserAgentService类

先搜集一些已知的UA信息写到一个 txt 文件中,初始化时将UA信息加载到List中。

User Agent中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,User Agent也简称UA。它是一个特殊字符串头,是一种向访问网站提供你所使用的浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。通过这个标 识,用户所访问的网站可以显示不同的排版从而为用户提供更好的体验或者进行信息统计;
例如用手机访问谷歌和电脑访问是不一样的,这些是谷歌根据访问者的 UA来判断的。UA可以进行伪装。
浏览器的UA字串的标准格式:浏览器标识 (操作系统标识; 加密等级标识; 浏览器语言) 渲染引擎标识版本信息。但各个浏览器有所不同。
@Service用于标注业务层组件,@Controller用于标注控制层组件(如struts中的action),@Repository用于标注数据访问组件,即DAO组件,而@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。

可以使用以下方式指定初始化方法和销毁方法(方法名任意):

1
2
3
4
5
6
7
8
@PostConstruct

public void init() {
}

@PreDestroy
public void destory() {
}
1
SpringContext.getBean()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@PostConstruct
public void init() {
List list = new ArrayList<>();
//获取userAgent.txt文件
String defaultPath = this.getClass().getResource("/useragents.txt").getPath();
//创建文件对象
File configFile = new File(defaultPath);
try {
list = FileUtils.readLines(configFile, "UTF-8");
if (list != null && !list.isEmpty()) {
//去重处理
LinkedHashSet set = new LinkedHashSet(list.size());
set.addAll(list);
list.clear();
list.addAll(set);
userAgentList.addAll(list);
}
} catch (IOException e) {
log.error("read user agent config file meet error:", e);
}
log.info("init userAgent config finished, userAgent list size=" + (list == null ? 0 : list.size()));
}

public List getUserAgentList() {
return userAgentList;
}
}

构建代理的java原生方法

1
2
3
4
public HttpHost toHost(){
return new HttpHost(address, port, "http");
}
//可以指定协议类型,但不支持SOCKS协议

定义http的代理类

1
2
3
4
5
6
7
8
9
10
public class HttpProxy {
private String address; //地址

private int port; //端口

private String type = "http"; //类型

private String provider; //代理商

private boolean isValid;

工厂方法

定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行。
如果都是未知的属性,可以使用接口,如果存在已知的属性,使用抽象类更好。

存储数据

1
2
3
4
5
public interface Store {

public void store(Object obj);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MySQLStore implements Store {

private QueryRunner queryRunner = new QueryRunner(DBCPUtil.getDataSource());

@Override
public void store(Page page) {

String sql = "insert into phone(id, source, brand, title, price, comment_count, url, img_url, params) values(?, ?, ?, ?, ?, ?, ?, ?, ?)";
try {
queryRunner.update(sql, page.getId(),
page.getSource(),
page.getBrand(),
page.getTitle(),
page.getPrice(),
page.getCommentCount(),
page.getUrl()
page.getImgUrl(),
page.getParams());
} catch (SQLException e) {
e.printStackTrace();
}
}
}

DBCP工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DBCPUtil {

private static DataSource ds;
static {
try {
InputStream in = DBCPUtil.class.getClassLoader().getResourceAsStream("Mysql_Config.properties");
Properties props = new Properties();
props.load(in);
ds = BasicDataSourceFactory.createDataSource(props);

} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

public static DataSource getDataSource() {
return ds;
}

public static Connection getConnection() {
try {
return ds.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

数据库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#连接设置

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spider_test
username=root
password=admin

#
initialSize=10

#最大连接数量
maxActive=50

#
maxIdle=20

#
minIdle=5

#
maxWait=60000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class JvmUtils { 

private static final String methodName= "appendToClassPathForInstrumentation";
/**

* 适用于 JDK 1.6 from JDK DOC "java.lang.instrument Interface Instrumentation" ...
* The system class loader supports
* adding a JAR file to be searched if it implements a method named appendToClassPathForInstrumentation which takes
* a single parameter of type java.lang.String. The method is not required to have public access. The name of the
* JAR file is obtained by invoking the getName() method on the jarfile and this is provided as the parameter to the
* appendtoClassPathForInstrumentation method.
* 将指定文件加载到classpath
* @param name 指定路径
* @return 如果加载成功返回true
*/

public static boolean appendToClassPath(String name){
try {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Method appendToClassPathMethod = classLoader.getClass()
.getDeclaredMethod(methodName,String.class);

if(null!=appendToClassPathMethod){
appendToClassPathMethod.setAccessible(true); // 开启权限
appendToClassPathMethod.invoke(classLoader,name);
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
*
* 递归 加载 jar 包
* @param dirName
* @return
*/
public static String[] addAllJarsToClassPath(String dirName){

List ret = new ArrayList<>();
File file = new File(dirName);
if(file.isDirectory()){
File[] files = file.listFiles();
for (File f:files) {
if(f.isDirectory()){
ret.addAll(Arrays.asList(addAllJarsToClassPath(f.getAbsolutePath())));

}else {




String filename= f.getName().toLowerCase();




if(filename.endsWith(".jar")){




if(appendToClassPath(f.getAbsolutePath())){




ret.add(f.getAbsolutePath());

}
}
}
}
}
return ret.toArray(new String[0]);
}
}
0%