Python,爬虫与深度学习(6)——爬虫详解(二)构建自己的ip池(简易版)
步骤/目录:
1.需求分析
2.mysql建表
3.网页请求分析
4.爬虫编写
(0)安装所需模块
(1)mysql存取
(2)爬虫主体编写
(3)其他爬虫补充
(4)实际运行
(5)检测爬取的ip
5.总结与改进
本文首发于个人博客https://lisper517.top/index.php/archives/44/
,转载请注明出处。
本文的目的是构建自己的ip池。
本文写作日期为2022年9月4日。操作的平台为win10,编辑器为VS code。
爬虫写的好,牢房进的早。使用爬虫时不要占用服务器太多流量,否则可能惹祸上身。
除了python与爬虫,这里实际上还需要了解html格式与mysql命令。可以在 runoob-html 或 runoob-mysql 找到初步的学习资料,或者查看笔者其他的文章。
最后,笔者的能力有限,也并非CS从业人员,很多地方难免有纰漏或者不符合代码原则的地方,请在评论中指出。
1.需求分析
之前说过,在对别人的网页服务器进行爬取时要注意不能占用太多服务器资源。但有时候,我们确实需要在短时间内对某个网页高频访问,又担心自己被封ip,那么此时维护一个ip池就非常有必要,我们需要对网络上的ip(每个ip背后是一个或一群服务器)进行测试,看是否能利用这些ip作为代理服务器访问其他网页。本文先介绍如何爬取ip,后续文章会介绍如何用自己的ip池爬取网页,以及代理失效时更换代理。在爬虫的一些较高级的应用中,一个ipool是非常基础且不可或缺的,所以建议学会scrapy后就应该尽快建立自己的ipool(除非没有高频访问某个网页的需求)。
ip池的构建和使用分为两步,第一步是筛选ip。世界上的公网ipv4地址最多有 4,294,967,296 个(256^4。实际上有一些被局域网占用,有一些不会使用),要一个个去验证这些ip耗时太长,何况还需要对已存在的ip进行验证、剔除失效ip。所以,这里使用一些提供免费代理ip的网页缩小ip搜索范围,爬取到的代理ip不做验证,都放入库中(这里使用mysql)。
第二步是对mysql中已有的ip进行验证,挑选出对某个网址有效的代理ip。这一步严格来讲和第一步关系不大,它可以在前一个步骤进行时也同步进行,更重要的是这一步是在其他爬虫开始后才进行,下面就讲解一下免费代理ip的使用。
免费代理可分为HTTP或HTTPS,高匿名和低匿名。如果爬虫运行时经过了这些免费代理,毕竟还是别人的服务器,你的请求和响应是到了别人那里,所以用代理服务器时不要传递一些私密信息。实在有必要的话,自己买服务器做转发。最后,同一个代理ip,可能你用它代理一个网站可以,代理另一个网站就不行,这也是不确定因素,所以爬到的代理ip在使用时还要对特定的网址进行测试,其实也是比较麻烦的,如果有紧急需求,建议买一些付费的代理ip。
2.mysql建表
在mysql中存储收集到的ip,需要新建以下列:
ip,字符串形式,VARCHAR(50)(为了将来兼容ipv6,稍微长一点);
端口号,数值形式,SMALLINT UNSIGNED(刚好是0~65535);
匿名度,2个bit位,BIT(2)(0表示低匿名,1表示高匿名,3表示未知);
类型(HTTP还是HTTPS),2个bit位,BIT(2)(0表示HTTP,1表示HTTPS,2表示都可,3表示其他);
ip物理位置,字符串,VARCHAR(100);
响应速度,MEDIUMINT,单位是毫秒ms;
最后一次验证的时间,日期时间格式。
建表的语句在程序中将给出。其实对这种不断更新、添加的数据,redis更加合适,有关redis的内容将在后续的文章中介绍。
3.网页请求分析
这里使用的提供代理ip的网站为(笔者给出的例子中,只使用了第一个网址):
http://www.ip3366.net/free/
https://www.kuaidaili.com
https://ip.jiangxianli.com/
https://www.cool-proxy.net/
https://free-proxy-list.net
https://www.ipaddress.com/proxy-list/
http://proxy-list.org/english/
https://www.proxynova.com/proxy-server-list/
http://pubproxy.com/api/proxy?limit=5&format=txt&type=http&level=anonymous&last_check=60&no_country=CN
https://www.rmccurdy.com/.scripts/proxy/good.txt
http://spys.one/en/anonymous-proxy-list/
https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.json
https://raw.githubusercontent.com/a2u/free-proxy-list/master/free-proxy-list.txt
https://raw.githubusercontent.com/clarketm/proxy-list/master/proxy-list.txt
https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/http.txt
这些网页的请求响应都挺简单的,这里就不专门介绍了。由于涉及多个网站,这里将使用多线程的方法,持续对各个ip网站爬取。唯一需要注意的是对每个网站,爬取ip的速度不要太快了,虽然对单个网站爬取速度不快,但毕竟有这么多ip网站,实际上ip入库的速度挺快的。
4.爬虫编写
(0)安装所需模块
需要用到requests(处理网络请求)、bs4(对响应内容进行解析)、pymysql(连接mysql数据库)、lxml(网页解析器)、cryptography(连接mysql时密码加密)库,在cmd中使用 pip install requests bs4 pymysql lxml cryptography 下载(下载失败就一直使用上述命令,直到下载成功)。之后新建一个文件夹,用VS Code打开该文件夹、新建一个py文件,进行下面的操作。
(1)mysql存取
使用mysql存储ip,其他爬虫需要通过这个类存取ip。自己使用时,记得把mysql连接的地址、端口、密码更改一下。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import pymysql, time, os, logging
from traceback import print_exc
if not os.path.isdir(os.path.join(os.getcwd(), 'logs')):
os.mkdir(os.path.join(os.getcwd(), 'logs'))
class MysqlOperationFailed(Exception):
"""若更改mysql出现错误则抛出这个异常。"""
def __init__(self):
message = "Failed to change data in MySQL, please check. "
super().__init__(message)
class mysql_api():
"""通过这个界面实现提交爬取的ip、取出ip的操作。"""
conn = pymysql.connect(host='127.0.0.1',
port=53306,
user='root',
passwd='mysqlpasswd',
db='mysql',
charset='utf8')
cur = conn.cursor()
def __init__(self, log_name: str):
"""初始函数,需要提交一个log_name作为进程的log文件名。"""
self.cur = mysql_api.conn.cursor()
mysql_api.check_table()
self.logging = logging.basicConfig(
level=logging.INFO,
filename=os.path.join(
os.getcwd(), 'logs',
'{}-log{}.txt'.format(log_name,
time.strftime("%Y-%m-%d-%H_%M_%S"))))
def __del__(self):
self.cur.close()
@staticmethod
def check_table():
"""检查一下mysql中是否已经建好了表,若表不存在则新建表。这里并没有检查:若表存在,表的格式是否正确。"""
cur, conn = mysql_api.cur, mysql_api.conn
command = '''
CREATE TABLE IF NOT EXISTS ipool (
no BIGINT UNSIGNED AUTO_INCREMENT,
ip VARCHAR(50) NOT NULL DEFAULT "0.0.0.0" COMMENT "IP address",
port SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "port",
anonymous BIT(2) NOT NULL DEFAULT 3 COMMENT "whether ip is anonymous",
type BIT(2) NOT NULL DEFAULT 3 COMMENT "HTTP or HTTPS or both or others",
physical_address VARCHAR(100) NOT NULL DEFAULT "unknown" COMMENT "where is the server",
response_time MEDIUMINT NOT NULL DEFAULT -1 COMMENT "response_time in microseconds",
last_verify DATETIME NOT NULL DEFAULT '1000-01-01 00:00:00' COMMENT "last verify time",
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (no));'''
command = command.replace('\n', '')
command = command.replace(' ', '')
try:
cur.execute("CREATE DATABASE IF NOT EXISTS ipool;")
cur.execute("USE ipool;")
cur.execute(command)
conn.commit()
except:
conn.rollback()
raise MysqlOperationFailed
def save_ips(self, ips_list: list):
"""存入多个ip(以字典的列表形式提交)。注意这里只是对不存在的数据添加默认值,没有验证数据类型等。
另外注意,响应时间单位为ms。"""
cur, conn = self.cur, mysql_api.conn
command = '''INSERT INTO ipool (ip,port,anonymous,type,physical_address,response_time,last_verify) VALUES '''
for ip_dict in ips_list:
ip, port, anonymous, type_, physical_address, response_time, last_verify = "0.0.0.0", 0, 3, 3, 'unknown', -1, "1000-01-01 00:00:00"
if 'ip' in ip_dict.keys():
ip = ip_dict['ip']
if 'port' in ip_dict.keys():
port = ip_dict['port']
if 'anonymous' in ip_dict.keys():
anonymous = ip_dict['anonymous']
if 'type' in ip_dict.keys():
type_ = ip_dict['type']
if 'physical_address' in ip_dict.keys():
physical_address = ip_dict['physical_address']
if 'response_time' in ip_dict.keys():
response_time = ip_dict['response_time']
if 'last_verify' in ip_dict.keys():
last_verify = ip_dict['last_verify']
command += '("{}",{},{},{},"{}",{},"{}"),'.format(
ip, port, anonymous, type_, physical_address, response_time,
last_verify)
command = command[:-1] + ';'
try:
cur.execute(command)
conn.commit()
except:
conn.rollback()
raise MysqlOperationFailed
@staticmethod
def check_repeat():
"""检查爬到的数据里有没有重复的,只检查ip和port都相同的。如果有重复的,保留最新的一条记录,也就是no最大的。"""
cur, conn = mysql_api.cur, mysql_api.conn
cur.execute('USE ipool;')
cur.execute(
'SELECT ip,port FROM ipool GROUP BY ip,port HAVING COUNT(*)>1;')
repeated_items = cur.fetchall()
if len(repeated_items) == 0:
print('\n数据库中无重复数据。')
return
else:
print('\n数据库中检测到重复数据,有{}种。'.format(len(repeated_items)))
command = '''
DELETE FROM ipool WHERE no IN (SELECT no FROM
(SELECT no FROM ipool WHERE
(ip,port) IN (SELECT ip,port FROM ipool GROUP BY ip,port HAVING COUNT(*)>1)
AND no NOT IN (SELECT MAX(no) FROM ipool GROUP BY ip,port HAVING COUNT(*)>1)) AS a);'''
command = command.replace('\n', '')
command = command.replace(' ', '')
try:
cur.execute(command)
conn.commit()
except:
conn.rollback()
raise MysqlOperationFailed
@staticmethod
def get_ips(anonymous: list = [1],
type_: list = [2],
response_time: int = 30000,
last_verify_interval: str = '48:00:00',
limit: int = 1000) -> tuple:
"""根据要求返回多个ip,只返回ip和port。
:param last_verify_interval 用于指定上次验证的时间距现在不超过多久。"""
cur, conn = mysql_api.cur, mysql_api.conn
cur.execute('USE ipool;')
anonymous, type_ = tuple(anonymous), tuple(type_)
command = '''
SELECT ip,port FROM ipool
WHERE anonymous IN {} AND type IN {}
AND response_time BETWEEN 0 AND {} AND (NOW()-last_verify)<"{}"
ORDER BY response_time LIMIT {};
'''.format(anonymous, type_, response_time, last_verify_interval,
limit)
command = command.replace('\n', '')
command = command.replace(' ', '')
print(command)
cur.execute(command)
return cur.fetchall()
if __name__ == "__main__":
try:
a = mysql_api('kuaidaili')
ip_list = []
for i in range(1, 256):
ip_dict = {
'ip': '192.168.1.{}'.format(i),
'port': i,
'response_time': i * 10,
'last_verify': '2022-09-05 12:00:00'
}
ip_list.append(ip_dict)
a.save_ips(ip_list)
a.check_repeat()
print(a.get_ips([0, 1, 2, 3], [0, 1, 2, 3], 30000))
except:
print_exc()
(2)爬虫主体编写
这里稍微把前面mysql存取的部分进行了更改,一并写出。单个ip网站的爬虫使用了递归调用的方法,不太适合页码比较多的情况。
这里只是写了对 http://www.ip3366.net/free/ 的爬虫、并用线程的方法开启它,其他爬虫可在后面自行补充。
最后,自己使用时,记得把mysql连接的地址、端口、密码更改一下。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import pymysql, os, time, logging
from traceback import print_exc
if not os.path.isdir(os.path.join(os.getcwd(), 'logs')):
os.mkdir(os.path.join(os.getcwd(), 'logs'))
logging.basicConfig(level=logging.INFO,
filename=os.path.join(
os.getcwd(), 'logs', 'log-{}.txt'.format(
time.strftime("%Y-%m-%d-%H_%M_%S"))))
class MysqlOperationFailed(Exception):
"""若更改mysql出现错误则抛出这个异常。"""
def __init__(self):
message = "Failed to change data in MySQL, please check. "
super().__init__(message)
class mysql_api():
"""通过这个界面实现提交爬取的ip、取出ip的操作。"""
conn = pymysql.connect(host='127.0.0.1',
port=53306,
user='root',
passwd='mysqlpasswd',
db='mysql',
charset='utf8')
cur = conn.cursor()
def __init__(self):
self.cur = mysql_api.conn.cursor()
mysql_api.check_table()
message = 'one of mysql_api object started'
logging.info(message)
def __del__(self):
self.cur.close()
message = 'one of mysql_api object stopped'
logging.info(message)
@staticmethod
def check_table():
"""检查一下mysql中是否已经建好了表,若表不存在则新建表。这里并没有检查:若表存在,表的格式是否正确。"""
cur, conn = mysql_api.cur, mysql_api.conn
command = '''
CREATE TABLE IF NOT EXISTS ipool (
no BIGINT UNSIGNED AUTO_INCREMENT,
ip VARCHAR(50) NOT NULL DEFAULT "0.0.0.0" COMMENT "IP address",
port SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "port",
anonymous BIT(2) NOT NULL DEFAULT 3 COMMENT "whether ip is anonymous",
type BIT(2) NOT NULL DEFAULT 3 COMMENT "HTTP or HTTPS or both or others",
physical_address VARCHAR(100) NOT NULL DEFAULT "unknown" COMMENT "where is the server",
response_time MEDIUMINT NOT NULL DEFAULT -1 COMMENT "response_time in microseconds",
last_verify DATETIME NOT NULL DEFAULT '1000-01-01 00:00:00' COMMENT "last verify time",
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (no));'''
command = command.replace('\n', '')
command = command.replace(' ', '')
try:
cur.execute("CREATE DATABASE IF NOT EXISTS ipool;")
cur.execute("USE ipool;")
cur.execute(command)
conn.commit()
message = 'database ipool and table ipool checked'
logging.info(message)
except:
conn.rollback()
raise MysqlOperationFailed
def save_ips(self, ips_list: list):
"""存入多个ip(以字典的列表形式提交)。注意这里只是对不存在的数据添加默认值,没有验证数据类型等。
另外注意,响应时间单位为ms。"""
cur, conn = self.cur, mysql_api.conn
command = '''INSERT INTO ipool (ip,port,anonymous,type,physical_address,response_time,last_verify) VALUES '''
for ip_dict in ips_list:
ip, port, anonymous, type_, physical_address, response_time, last_verify = "0.0.0.0", 0, 3, 3, 'unknown', -1, "1000-01-01 00:00:00"
if 'ip' in ip_dict.keys():
ip = ip_dict['ip']
if 'port' in ip_dict.keys():
port = ip_dict['port']
if 'anonymous' in ip_dict.keys():
anonymous = ip_dict['anonymous']
if 'type' in ip_dict.keys():
type_ = ip_dict['type']
if 'physical_address' in ip_dict.keys():
physical_address = ip_dict['physical_address']
if 'response_time' in ip_dict.keys():
response_time = ip_dict['response_time']
if 'last_verify' in ip_dict.keys():
last_verify = ip_dict['last_verify']
command += '("{}",{},{},{},"{}",{},"{}"),'.format(
ip, port, anonymous, type_, physical_address, response_time,
last_verify)
message = 'trying to save ip:port={}:{}'.format(ip, port)
logging.info(message)
command = command[:-1] + ';'
try:
cur.execute(command)
conn.commit()
message = 'saving ips successfully'
logging.info(message)
except:
conn.rollback()
raise MysqlOperationFailed
def check_repeat(self):
"""检查爬到的数据里有没有重复的,只检查ip和port都相同的。如果有重复的,保留最新的一条记录,也就是no最大的。"""
cur, conn = mysql_api.cur, mysql_api.conn
cur.execute('USE ipool;')
cur.execute(
'SELECT ip,port FROM ipool GROUP BY ip,port HAVING COUNT(*)>1;')
repeated_items = cur.fetchall()
if len(repeated_items) == 0:
message = 'no repeated ip:port in ipool'
logging.info(message)
return
else:
message = 'found ip:port repeated {} kinds'.format(
len(repeated_items))
logging.info(message)
command = '''
DELETE FROM ipool WHERE no IN (SELECT no FROM
(SELECT no FROM ipool WHERE
(ip,port) IN (SELECT ip,port FROM ipool GROUP BY ip,port HAVING COUNT(*)>1)
AND no NOT IN (SELECT MAX(no) FROM ipool GROUP BY ip,port HAVING COUNT(*)>1)) AS a);'''
command = command.replace('\n', '')
command = command.replace(' ', '')
try:
cur.execute(command)
conn.commit()
message = 'repeated ip:port deleted successfully. '
logging.info(message)
except:
conn.rollback()
raise MysqlOperationFailed
@staticmethod
def get_ips(anonymous: list = [1],
type_: list = [2],
response_time: int = 30000,
last_verify_interval: str = '48:00:00',
limit: int = 1000) -> tuple:
"""根据要求返回多个ip,只返回ip和port。
:param last_verify_interval 用于指定上次验证的时间距现在不超过多久。"""
cur, conn = mysql_api.cur, mysql_api.conn
cur.execute('USE ipool;')
anonymous, type_ = tuple(anonymous), tuple(type_)
command = '''
SELECT ip,port FROM ipool
WHERE anonymous IN {} AND type IN {}
AND response_time BETWEEN 0 AND {} AND (NOW()-last_verify)<"{}"
ORDER BY response_time LIMIT {};
'''.format(anonymous, type_, response_time, last_verify_interval,
limit)
command = command.replace('\n', '')
command = command.replace(' ', '')
print(command)
cur.execute(command)
ips = cur.fetchall()
return ips
import threading, requests, re
from lxml import etree
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
}
class spider_threads():
"""爬虫类,通过多线程的方法同时爬取多个网站。"""
websites = [
'http://www.ip3366.net/free/'
]
url_spidered_list = []
for i in range(len(websites)):
url_spidered_list.append(set())
def __init__(self):
self.threads = len(spider_threads.websites)
@staticmethod
def get_session():
"""返回一个session对象。"""
session = requests.Session()
session.mount('http://', requests.adapters.HTTPAdapter(max_retries=3))
session.mount('https://', requests.adapters.HTTPAdapter(max_retries=3))
return session
class spider_website():
"""定义一个网站爬虫的基本操作。"""
def __init__(self, no: int):
self.session = spider_threads.get_session()
self.base_url = spider_threads.websites[no]
self.url_spidered = spider_threads.url_spidered_list[no]
self.kind = re.search(r'\.{1}.+\.{1}',
self.base_url).group().replace('.', '')
self.mysql_api = mysql_api()
def get_html(self, url):
"""对一个页面,返回其响应对象。"""
global headers
session = self.session
try:
html = session.get(url, timeout=30, headers=headers)
except requests.exceptions.TooManyRedirects as e:
message = "网页重定向,休息10min({})。".format(self.kind)
logging.warning(message)
print('\r' + message, end='')
time.sleep(600)
html = session.get(url, timeout=30, headers=headers)
html.encoding = html.apparent_encoding
return html
def get_ip_recursively(self, url):
"""这个方法用于递归调用爬虫,将在子类中重写。"""
pass
def start_spider(self):
"""这个方法用于开启爬虫,将在子类中重写。"""
pass
class spider_ip3366(spider_website):
"""ip3366的爬虫类。"""
def __init__(self):
super().__init__(0)
self.start_urls = [
self.base_url, 'http://www.ip3366.net/free/?stype=2'
]
def get_ip_recursively(self, url):
"""这是一个递归调用的爬虫。它将爬取页面上的信息,将爬取过的网页存入字典,对页面上新的网页进行爬取。
重写了父类方法。"""
html = self.get_html(url)
time.sleep(15)
message = '1 page crawled:{}'.format(url)
logging.info(message)
html = etree.HTML(html.content)
ip_nodes = html.xpath(r'//*[@id="list"]/table/tbody/tr')
ips_list = []
for ip_node in ip_nodes:
ip_dict = {}
ip = ip_node.xpath(r'./td[1]/text()')[0].strip()
port = int(ip_node.xpath(r'./td[2]/text()')[0].strip())
anonymous = (0, 1)['高匿' in ip_node.xpath(r'./td[3]/text()')[0]]
type_ = (0, 1)['HTTPS' in ip_node.xpath(r'./td[4]/text()')[0]]
physical_address = ip_node.xpath(r'./td[5]/text()')[0].strip()
response_time = 1000 * int(
ip_node.xpath(r'./td[6]/text()')[0].strip().replace(
'秒', ''))
response_time = (response_time, 250)[response_time == 0]
last_verify = ip_node.xpath(
r'./td[7]/text()')[0].strip().replace('/', '-')
ip_dict['ip'] = ip
ip_dict['port'] = port
ip_dict['anonymous'] = anonymous
ip_dict['type'] = type_
ip_dict['physical_address'] = physical_address
ip_dict['response_time'] = response_time
ip_dict['last_verify'] = last_verify
ips_list.append(ip_dict)
self.mysql_api.save_ips(ips_list)
self.url_spidered.add(url)
page_urls = html.xpath(r'//*[@id="listnav"]/ul/a/@href')
for i in range(len(page_urls)):
page_url = self.base_url + page_urls[i]
if page_url not in self.url_spidered:
self.get_ip_recursively(page_url)
else:
return
def start_spider(self):
"""这个方法用于开启爬虫,对父类的同名方法进行重写。"""
for start_url in self.start_urls:
self.get_ip_recursively(start_url)
self.mysql_api.check_repeat()
def start_all_spider_process():
process_list = []
process_list.append(crawl_website_ip3366().start_spider)
for i in process_list:
process_pool.apply_async(i)
time.sleep(35)
process_pool.close()
process_pool.join()
if __name__ == "__main__":
try:
start_all_spider_process()
except:
traceback.print_exc()
spider_ip3366类是一个范例,其他爬虫也照着这样写就行了,所以剩下的只是简单的重复劳动,这里笔者就不进行展示了。
如果要继续写的话,注意其他网站的爬虫都和spider_ip3366类处于同一个层级,也在spider_threads内部。添加其他网站的爬虫后,在start_all_spider_threads里添加线程即可。
(4)实际运行
自行添加其他网站的爬虫后,运行。
(5)检测爬取的ip
这是相对独立的一个步骤,并且要在使用时同步进行,将在后续的文章中讲解。
5.总结与改进
(1)这个爬虫使用了递归,在内存小一点的机器上其实不太好运行。有可能的话,可以改用递归外的方法(比如用scrapy),专门买一个更加小巧的机器或云服务器,全天运行这个爬虫和ip检验,以便能及时更新自己的ipool。
(2)在上一个例子( Python,爬虫与深度学习(2)——爬虫详解(一)爬取金融机构成交持仓排名 )和本例中,都用到了一些重复的方法,比如连接mysql时添加conn、cur,以及log操作。这部分其实可以在自建库里写方法调用,也免得每次都要更改。
(3)这里所有爬虫使用的log文件都是同一个,不太容易看清楚。后续可以让每个爬虫使用不同的log文件。
(4)除了自己写这种ipool爬虫,其实在github上有其他人做好的成品。笔者在这里推荐两个项目: https://github.com/SpiderClub/haipproxy 和 https://github.com/imWildCat/scylla ,这两个项目都可以用docker安装。
更新:docker-scylla可能自带挖矿病毒,详见这个问题: https://github.com/imWildCat/scylla/issues/102 。