首页 文章 精选 留言 我的

精选列表

搜索[快速入门],共10000篇文章
优秀的个人博客,低调大师

Python爬虫入门教程 3-100 美空网数据爬取

1.美空网数据-简介 从今天开始,我们尝试用2篇博客的内容量,搞定一个网站叫做“美空网”网址为:http://www.moko.cc/, 这个网站我分析了一下,我们要爬取的图片在 下面这个网址 http://www.moko.cc/post/1302075.html 然后在去分析一下,我需要找到一个图片列表页面是最好的,作为一个勤劳的爬虫coder,我找到了这个页面 http://www.moko.cc/post/da39db43246047c79dcaef44c201492d/list.html 列表页面被我找到了,貌似没有分页,这就简单多了,但是刚想要爬,就翻车了,我发现一个严重的问题。 http://www.moko.cc/post/==da39db43246047c79dcaef44c201492d==/list.html 我要做的是一个自动化的爬虫,但是我发现,出问题了,上面那个黄色背景的位置是啥? ID,昵称,个性首页,这个必须要搞定。 我接下来随机的找了一些图片列表页,试图找到规律到底是啥? http://www.moko.cc/post/978c74a0375f4edca114e87b0a45a0b5/list.html http://www.moko.cc/post/jundayi/list.html http://www.moko.cc/post/slavik/list.html ...... 没什么问题,发现规律了 http://www.moko.cc/post/==个性昵称(中文昵称是一个加密的串)==/list.html 这就有点意思了,我要是能找到尽量多的昵称,不就能拼接出来我想要得所有地址了吗 开干!!! 手段,全站乱点,找入口,找切入点,找是否有API .... .... 结果没找着 下面的一些备选方案 趴这个页面,发现只有 20页 http://www.moko.cc/channels/post/23/1.html 每页48个模特,20页。那么也才960人啊,完全覆盖不到尽可能多的用户。 接着又找到 http://www.moko.cc/catalog/index.html 这个页面 确认了一下眼神,以为发现问题了,结果 哎呀,还么有权限,谁有权限,可以跟我交流一下,一时激动,差点去下载他们的APP,然后进行抓包去。 上面两条路,都不好弄,接下来继续找路子。 无意中,我看到了一丝曙光 关注名单,点进去 哈哈哈,OK了,这不就是,我要找到的东西吗? 不多说了,爬虫走起,测试一下他是否有反扒机制。 我找到了一个关注的人比较多的页面,1500多个人 http://www.moko.cc/subscribe/chenhaoalex/1.html 然后又是一波分析操作 2.美空网数据- 爬虫数据存储 确定了爬虫的目标,接下来,我做了两件事情,看一下,是否对你也有帮助 确定数据存储在哪里?最后我选择了MongoDB 用正则表达式去分析网页数据 对此,我们需要安装一下MongoDB,安装的办法肯定是官网教程啦! https://docs.mongodb.com/master/tutorial/install-mongodb-on-red-hat/ 如果官方文档没有帮助你安装成功。 那么我推荐下面这篇博客 https://www.cnblogs.com/hackyo/p/7967170.html 安装MongoDB出现如下结果 恭喜你安装成功了。 接下来,你要学习的是 关于mongodb用户权限的管理 http://www.cnblogs.com/shiyiwen/p/5552750.html mongodb索引的创建 https://blog.csdn.net/salmonellavaccine/article/details/53907535 别问为啥我不重新写一遍,懒呗~~~ 况且这些资料太多了,互联网大把大把的。 一些我经常用的mongdb的命令 链接 mongo --port <端口号> 选择数据库 use admin 展示当前数据库 db 当前数据库授权 db.auth("用户名","密码") 查看数据库 show dbs 查看数据库中的列名 show collections 创建列 db.createCollection("列名") 创建索引 db.col.ensureIndex({"列名字":1},{"unique":true}) 展示所有索引 db.col.getIndexes() 删除索引 db.col.dropIndex("索引名字") 查找数据 db.列名.find() 查询数据总条数 db.列名.find().count() 上面基本是我最常用的了,我们下面实际操作一把。 用Python链接MongoDB 使用 pip3 安装pymongo库 使用pymongo模块连接mongoDB数据库 一些准备工作 创建dm数据库 链接上mongodb 在终端使用命令 mongo --port 21111 [linuxboy@localhost ~]$ mongo --port 21111 MongoDB shell version v3.6.5 connecting to: mongodb://127.0.0.1:21111/ MongoDB server version: 3.6.5 > 配置用户权限:接着上面输入命令 show dbs 查看权限 权限不足 创建管理用户 db.createUser({user: "userAdmin",pwd: "123456", roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] } ) 授权用户 db.auth("userAdmin","123456") 查看权限 > db.auth("userAdmin","123456") 1 > show dbs admin 0.000GB config 0.000GB local 0.000GB moko 0.013GB test 0.000GB > 接下来创建 dm数据库<在这之前还需要创建一个读写用户> > use dm switched to db dm > db dm > db.createUser({user: "dba",pwd: "dba", roles: [ { role: "readWrite", db: "dm" } ] } ) Successfully added user: { "user" : "dba", "roles" : [ { "role" : "readWrite", "db" : "dm" } ] } > 重新授权 db.auth("dba","dba") 创建一列数据 > db.createCollection("demo") { "ok" : 1 } > db.collections dm.collections > show collections demo > Python实现插入操作 import pymongo as pm #确保你已经安装过pymongo了 # 获取连接 client = pm.MongoClient('localhost', 21111) # 端口号是数值型 # 连接目标数据库 db = client.dm # 数据库用户验证 db.authenticate("dba", "dba") post = { "id": "111111", "level": "MVP", "real":1, "profile": '111', 'thumb':'2222', 'nikename':'222', 'follows':20 } db.col.insert_one(post) # 插入单个文档 # 打印集合第1条记录 print (db.col.find_one()) 编译执行 [linuxboy@bogon moocspider]$ python3 mongo.py {'_id': ObjectId('5b15033cc3666e1e28ae5582'), 'id': '111111', 'level': 'MVP', 'real': 1, 'profile': '111', 'thumb': '2222', 'nikename': '222', 'follows': 20} [linuxboy@bogon moocspider]$ 好了,我们到现在为止,实现了mongodb的插入问题。 3.美空网数据-用Python 爬取关注对象 首先,我需要创造一个不断抓取链接的类 这个类做的事情,就是分析 http://www.moko.cc/subscribe/chenhaoalex/1.html 这个页面,总共有多少页,然后生成链接 抓取页面中的总页数为77 正则表达式如下 onfocus=\"this\.blur\(\)\">(\d*?)< 在这里,由所有的分页都一样,所以,我匹配了全部的页码,然后计算了数组中的最大值 #获取页码数组 pages = re.findall(r'onfocus=\"this\.blur\(\)\">(\d*?)<',content,re.S) #获取总页数 page_size = 1 if pages: #如果数组不为空 page_size = int(max(pages)) #获取最大页数 接下来就是我们要搞定的生产者编码阶段了,我们需要打造一个不断获取连接的爬虫 简单的说就是 我们需要一个爬虫,不断的去爬取 http://www.moko.cc/subscribe/chenhaoalex/1.html 这个页面中所有的用户,并且还要爬取到总页数。 比如查看上述页面中,我们要获取的关键点如下 通过这个页面,我们要得到,这样子的一个数组,注意下面数组中有个位置【我用爬虫爬到的】这个就是关键的地方了 all_urls = [ "http://www.moko.cc/subscribe/chenhaoalex/1.html", "http://www.moko.cc/subscribe/chenhaoalex/2.html", "http://www.moko.cc/subscribe/chenhaoalex/3.html", "http://www.moko.cc/subscribe/chenhaoalex/4.html", ...... "http://www.moko.cc/subscribe/dde760d5dd6a4413aacb91d1b1d76721/1.html" "http://www.moko.cc/subscribe/3cc82db2231a4449aaa97ed8016b917a/1.html" "http://www.moko.cc/subscribe/d45c1e3069c24152abdc41c1fb342b8f/1.html" "http://www.moko.cc/subscribe/【我用爬虫爬到的】/1.html" ] 引入必备模块 # -*- coding: UTF-8 -*- import requests #网络请求模块 import random #随机模块 import re #正则表达式模块 import time #时间模块 import threading #线程模块 import pymongo as pm #mongodb模块 接下来,我们需要准备一个通用函数模拟UserAgent做一个简单的反爬处理 class Config(): def getHeaders(self): user_agent_list = [ \ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1" \ "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", \ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", \ "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", \ "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", \ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", \ "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", \ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", \ "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", \ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", \ "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", \ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", \ "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", \ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", \ "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", \ "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", \ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", \ "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24" ] UserAgent=random.choice(user_agent_list) headers = {'User-Agent': UserAgent} return headers 编写生产者的类和核心代码,Producer继承threading.Thread #生产者 class Producer(threading.Thread): def run(self): print("线程启动...") headers = Config().getHeaders() if __name__ == "__main__": p = Producer() p.start() 测试运行,一下,看是否可以启动 [linuxboy@bogon moocspider]$ python3 demo.py 线程启动... {'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24'} [linuxboy@bogon moocspider]$ 如果上面的代码没有问题,接下来就是我们爬虫代码部分了,为了方便多线程之间的调用,我们还是创建一个共享变量在N个线程之间调用 # -*- coding: UTF-8 -*- import requests import random import re import time import threading import pymongo as pm # 获取连接 client = pm.MongoClient('localhost', 21111) # 端口号是数值型 # 连接目标数据库 db = client.moko # 数据库用户验证 db.authenticate("moko", "moko") urls = ["http://www.moko.cc/subscribe/chenhaoalex/1.html"] index = 0 #索引 g_lock = threading.Lock() #初始化一个锁 #生产者 class Producer(threading.Thread): def run(self): print("线程启动...") headers = Config().getHeaders() print(headers) global urls global index while True: g_lock.acquire() if len(urls)==0: g_lock.release() continue page_url = urls.pop() g_lock.release() #使用完成之后及时把锁给释放,方便其他线程使用 response = "" try: response = requests.get(page_url,headers=headers,timeout=5) except Exception as http: print("生产者异常") print(http) continue content = response.text rc = re.compile(r'<a class=\"imgBorder\" href=\"\/(.*?)\" hidefocus=\"true\">') follows = rc.findall(content) print(follows) fo_url = [] threading_links_2 = [] for u in follows: this_url = "http://www.moko.cc/subscribe/%s/1.html" % u g_lock.acquire() index += 1 g_lock.release() fo_url.append({"index":index,"link":this_url}) threading_links_2.append(this_url) g_lock.acquire() urls += threading_links_2 g_lock.release() print(fo_url) try: db.text.insert_many(fo_url,ordered=False ) except: continue if __name__ == "__main__": p = Producer() p.start() 上面代码除了基本操作以外,我做了一些细小的处理 现在说明如下 fo_url.append({"index":index,"link":this_url}) 这部分代码,是为了消费者使用时候,方便进行查找并且删除操作而特意改造的,增加了一个字段index作为标识 第二个部分,插入数据的时候,我进行了批量的操作使用的是insert_many函数,并且关键的地方,我增加了一个ordered=False的操作,这个地方大家可以自行研究一下,我的目的是去掉重复数据,默认情况下insert_many函数如果碰到数据重复,并且在mongodb中创建了索引==创建索引的办法,大家自行翻阅文章上面==,那么是无法插入的,但是这样子会插入一部分,只把重复的地方略过,非常方便。 关于pymongo的使用,大家可以参考官网手册 这个是 pymongo的官方教程 http://api.mongodb.com/python/current/api/pymongo/collection.html?highlight=insert_many#pymongo.collection.Collection.insert_many MongoDB的手册大家也可以参考 https://docs.mongodb.com/manual/reference/method/db.collection.insertMany/ db.text.insert_many(fo_url,ordered=False ) 我们链接上MongoDB数据库,查询一下我们刚刚插入的数据 > show collections col links text > db.text moko.text > db.text.find() { "_id" : ObjectId("5b1789e0c3666e642364a70b"), "index" : 1, "link" : "http://www.moko.cc/subscribe/dde760d5dd6a4413aacb91d1b1d76721/1.html" } { "_id" : ObjectId("5b1789e0c3666e642364a70c"), "index" : 2, "link" : "http://www.moko.cc/subscribe/3cc82db2231a4449aaa97ed8016b917a/1.html" } ....... { "_id" : ObjectId("5b1789e0c3666e642364a71e"), "index" : 20, "link" : "http://www.moko.cc/subscribe/8c1e4c738e654aad85903572f9090adb/1.html" } Type "it" for more 其实上面代码,有一个非常严重的BUG,就是当我们实际操作的时候,发现,我们每次获取到的都是我们使用this_url = "http://www.moko.cc/subscribe/%s/1.html" % u 进行拼接的结果。 也就是说,我们获取到的永远都是第1页。这个按照我们之前设计的就不符合逻辑了, 我们还要获取到分页的内容,那么这个地方需要做一个简单的判断,就是下面的逻辑了。 ==如果完整代码,大家不知道如何观看,可以直接翻阅到文章底部,有对应的github链接== #如果是第一页,那么需要判断一下 #print(page_url) is_home =re.search(r'(\d*?)\.html',page_url).group(1) if is_home == str(1): pages = re.findall(r'onfocus=\"this\.blur\(\)\">(\d*?)<',content,re.S) #获取总页数 page_size = 1 if pages: page_size = int(max(pages)) #获取最大页数 if page_size > 1: #如果最大页数大于1,那么获取所有的页面 url_arr = [] threading_links_1 = [] for page in range(2,page_size+1): url = re.sub(r'(\d*?)\.html',str(page)+".html",page_url) threading_links_1.append(url) g_lock.acquire() index += 1 g_lock.release() url_arr.append({ "index":index, "link": url}) g_lock.acquire() urls += threading_links_1 # URL数据添加 g_lock.release() try: db.text.insert_many(url_arr,ordered=False ) except Exception as e: print("数据库输入异常") print (e) continue else: pass else: pass 截止到现在为止,其实你已经实现了链接的生产者了 。 我们在MongoDB中生成了一堆链接,接下来就是使用阶段了。 使用起来也是非常简单。 我先给大家看一个比较复杂的正则表达式爬虫写的好不好,正则表达式站很重要的比例哦~ divEditOperate_(?P<ID>\d*)[\"] .*>[\s\S]*?<p class=\"state\">.*?(?P<级别>\w*P).*</span></span>(?P<是否认证><br/>)?.*?</p>[\s\S]*?<div class=\"info clearfix\">[\s\S]*?<a class=\"imgBorder\" href=\"\/(?P<主页>.*?)\" hidefocus=\"true\">[\s\S]*?<img .*?src=\"(?P<头像>.*?)\".*?alt=\".*?\" title=\"(?P<昵称>.*?)\" />[\s\S]*?<p class=\"font12 lesserColor\">(?P<地点>.*?)&nbsp.*?<span class=\"font12 mainColor\">(?P<粉丝数目>\d*?)</span> 上面这个正则表达式,就是我为 http://www.moko.cc/subscribe/chenhaoalex/1.html 这个页面专门准备的。 这样子,我就可以直接获取到我想要的所有数据了。 消费者的代码如下 get_index = 0 #消费者类 class Consumer(threading.Thread): def run(self): headers = Config().getHeaders() global get_index while True: g_lock.acquire() get_index += 1 g_lock.release() #从刚才数据存储的列里面获取一条数据,这里用到find_one_and_delete方法 #get_index 需要声明成全局的变量 link = db.links.find_one_and_delete({"index":get_index}) page_url = "" if link: page_url = link["link"] print(page_url+">>>网页分析中...") else: continue response = "" try: response = requests.get(page_url,headers=headers,timeout=5) except Exception as http: print("消费者有异常") print(http) continue content = response.text rc = re.compile(r'divEditOperate_(?P<ID>\d*)[\"] .*>[\s\S]*?<p class=\"state\">.*?(?P<级别>\w*P).*</span></span>(?P<是否认证><br/>)?.*?</p>[\s\S]*?<div class=\"info clearfix\">[\s\S]*?<a class=\"imgBorder\" href=\"\/(?P<主页>.*?)\" hidefocus=\"true\">[\s\S]*?<img .*?src=\"(?P<头像>.*?)\".*?alt=\".*?\" title=\"(?P<昵称>.*?)\" />[\s\S]*?<p class=\"font12 lesserColor\">(?P<地点>.*?)&nbsp.*?<span class=\"font12 mainColor\">(?P<粉丝数目>\d*?)</span>') user_info = rc.findall(content) print(">>>>>>>>>>>>>>>>>>>>") users = [] for user in user_info: post = { "id": user[0], "level": user[1], "real":user[2], "profile": user[3], 'thumb':user[4], 'nikename':user[5], 'address':user[6], 'follows':user[7] } users.append(post) print(users) try: db.mkusers.insert_many(users,ordered=False ) except Exception as e: print("数据库输入异常") print (e) continue time.sleep(1) print("<<<<<<<<<<<<<<<<<<<<") 当你使用python3 demo.py 编译demo之后,屏幕滚动如下结果,那么你成功了。 接下来就可以去数据库查阅数据去了。 [linuxboy@bogon moocspider]$ python3 demo.py 线程启动... {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3'} http://www.moko.cc/subscribe/chenhaoalex/2.html>>>网页分析中... ['dde760d5dd6a4413aacb91d1b1d76721', '3cc82db2231a4449aaa97ed8016b917a', 'a1835464ad874eec92ccbb31841a7590', 'c9ba6a47a246494398d4e26c1e0b7e54', '902fe175e668417788a4fb5d4de7ab99', 'dcb8f11265594f17b821a6d90caf96a7', '7ea0a96621eb4ed99c9c642936559c94', 'd45c1e3069c24152abdc41c1fb342b8f', 'chenyiqiu', '798522844', 'MEERILLES', 'ddfd9e1f7dca4cffb2430caebd2494f8', 'd19cbd37c87e400e9da42e159560649b', 'ac07e7fbfde14922bb1d0246b9e4374d', '05abc72ac7bb4f738f73028fed17ac23', 'hanzhuoer', 'e12e15aaee654b8aa9f528215bc3294c', '3b6d8dc6fd814789bd484f393b5c9fa8', '83256b93a2f94f449ab75c730cb80a7b', '8c1e4c738e654aad85903572f9090adb'] [{'index': 77, 'link': 'http://www.moko.cc/subscribe/dde760d5dd6a4413aacb91d1b1d76721/1.html'}, {'index': 78, 'link': 'http://www.moko.cc/subscribe/3cc82db2231a4449aaa97ed8016b917a/1.html'}, {'index': 79, 'link': 'http://www.moko.cc/subscribe/a1835464ad874eec92ccbb31841a7590/1.html'}, {'index': 80, 'link': 'http://www.moko.cc/subscribe/c9ba6a47a246494398d4e26c1e0b7e54/1.html'}, {] >>>>>>>>>>>>>>>>>>>> [{'id': '3533155', 'level': 'MP', 'real': '', 'profile': 'b1a7e76455cc4ca4b81ed800ab68b308', 'thumb': 'http://img.mb.moko.cc/2018-02-17/d7db42d4-7f34-46d2-a760-c88eb90d6e0d.jpg', 'nikename': '模特九九', 'address': '大连', 'follows': '10'}, {'id': '3189865', 'level': 'VIP', 'real': '', 'profile': 'cfdf1482a9034f65a60bc6a1cf8d6a02', 'thumb': 'http://img.mb.moko.cc/2016-09-30/98c1ddd3-f9a8-4a15-a106-5d664fa7b558.jpg', 'nikename': '何应77', 'address': '杭州', 'follows': '219'}, {'id': '14886', 'level': 'VIP', 'real': '<br/>', 'profile': 'cndp', 'thumb': 'http://img2.moko.cc/users/0/49/14886/logo/img2_des_x3_10100286.jpg', 'nikename': '多拍PGirl', 'address': '北京', 'follows': '2331'}, {'id': '3539257', 'level': 'MP', 'real': '<br/>', 'profile': '605c8fb2824049aa841f21858a7fd142', 'thumb': 'http://img.mb.moko.cc/2018-02': 记得处理数据的时候去掉重复值 >show collections col links mkusers text > db.mkusers.find() { "_id" : ObjectId("5b17931ec3666e6eff3953bc"), "id" : "3533155", "level" : "MP", "real" : "", "profile" : "b1a7e76455cc4ca4b81ed800ab68b308", "thumb" : "http://img.mb.moko.cc/2018-02-17/d7db42d4-7f34-46d2-a760-c88eb90d6e0d.jpg", "nikename" : "模特九九", "address" : "大连", "follows" : "10" } { "_id" : ObjectId("5b17931ec3666e6eff3953bd"), "id" : "3189865", "level" : "VIP", "real" : "", "profile" : "cfdf1482a9034f65a60bc6a1cf8d6a02", "thumb" : "http://img.mb.moko.cc/2016-09-30/98c1ddd3-f9a8-4a15-a106-5d664fa7b558.jpg", "nikename" : "何应77", "address" : "杭州", "follows" : "219" } { "_id" : ObjectId("5b17931ec3666e6eff3953be"), "id" : "14886", "level" : "VIP", "real" : "<br/>", "profile" : "cndp", "thumb" : "http://img2.moko.cc/users/0/49/14886/logo/img2_des_x3_10100286.jpg", "nikename" : "多拍PGirl", "address" : "北京", "follows" : "2331" } { "_ 最后一步,如果你想要把效率提高,修改线程就好了 if __name__ == "__main__": for i in range(5): p = Producer() p.start() for i in range(7): c = Consumer() c.start() 经过3个小时的爬取,我获取了70000多美空的用户ID,原则上,你可以获取到所有的被关注者的,不过这些数据对我们测试来说,已经足够使用。

优秀的个人博客,低调大师

gitbook 入门教程之使用 gitbook-cli 开发电子书

gitbook 生成电子书主要有三种方式: gitbook-cli 命令行操作,简洁高效,适合从事软件开发的相关人员. gitbook-editor 编辑器操作,可视化编辑,适合无编程经验的文学创作者. gitbook.com 官网操作,在线编辑实时发布,适合无本地环境且科学上网的体验者. 本文主要讲解第一种 gitbook-cli 命令行操作流程,其他两种见另外两篇教程. gitbook 的一些常用命令 安装 gitbook-cli 脚手架工具 本机已安装 node.js 开发环境,安装完成后运行 gitbook -V 能够打印出版本信息,则表示安装成功. $ sudo npm install -g gitbook-cli 关于安装配置相关问题请参考 环境要求 初始化 gitbook 项目 初始化项目,按照 gitbook 规范会自动创建 README.md 和 SUMMARY.md 两个文件,具体用途见下文. 其实 SUMMARY.md 是电子书的章节目录,gitbook 会初始化相应的文件目录结构,所以主要是用于开发初始阶段. $ gitbook init 启动 gitbook 项目 启动本地服务,程序无报错则可以在浏览器预览电子书效果: http://localhost:4000 由于能够实时预览电子书效果,并且大多数开发环境搭建在本地而不是远程服务器中,所以主要用于开发调试阶段. $ gitbook serve 构建 gitbook 静态网页 构建静态网页而不启动本地服务器,默认生成文件存放在 _book/ 目录,当然输出目录是可配置的,暂不涉及,见高级部分. 输出静态网页后可打包上传到服务器,也可以上传到 github 等网站进行托管,因而主要用于发布准备阶段. $ gitbook build 章节小结 gitbook init 初始化 README.md 和 SUMMARY.md 两个文件. gitbook build 本地构建但不运行服务,默认输出到 _book/ 目录. gitbook serve 本地构建并运行服务,默认访问 http://localhost:4000 实时预览. # 创建 `gitbook` 演示项目 $ mkdir gitbook-demo # 初始化项目 $ gitbook init warn: no summary file in this book info: create README.md info: create SUMMARY.md info: initialization is finished # 启动本地服务器 $ gitbook serve Live reload server started on port: 35729 Press CTRL+C to quit ... info: 7 plugins are installed info: loading plugin "livereload"... OK info: loading plugin "highlight"... OK info: loading plugin "search"... OK info: loading plugin "lunr"... OK info: loading plugin "sharing"... OK info: loading plugin "fontsettings"... OK info: loading plugin "theme-default"... OK info: found 1 pages info: found 0 asset files info: >> generation finished with success in 1.2s ! Starting server ... Serving book on http://localhost:4000 # 查看当前目录结构 $ tree . ├── README.md ├── SUMMARY.md └── _book ├── gitbook │ ├── fonts │ │ └── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── gitbook-plugin-fontsettings │ │ ├── fontsettings.js │ │ └── website.css │ ├── gitbook-plugin-highlight │ │ ├── ebook.css │ │ └── website.css │ ├── gitbook-plugin-livereload │ │ └── plugin.js │ ├── gitbook-plugin-lunr │ │ ├── lunr.min.js │ │ └── search-lunr.js │ ├── gitbook-plugin-search │ │ ├── lunr.min.js │ │ ├── search-engine.js │ │ ├── search.css │ │ └── search.js │ ├── gitbook-plugin-sharing │ │ └── buttons.js │ ├── gitbook.js │ ├── images │ │ ├── apple-touch-icon-precomposed-152.png │ │ └── favicon.ico │ ├── style.css │ └── theme.js ├── index.html └── search_index.json 11 directories, 27 files $ gitbook 的目录结构说明 既然要书写一本电子书,那么起码的章节介绍和章节详情自然是必不可少的. 当然还有标题,作者和联系方式等个性化信息需要指定,如果不指定的话,一旦采用默认配合,八成不符合我们的预期,说不定都会变成匿名电子书?所以配置文件一般也是需要手动设置的! 真正可选的文件要数词汇表了,毕竟不是每一本电子书都有专业词汇需要去解释说明.如果在章节详情顺便解释下涉及到的专业词汇,那么自然也就不需要词汇表文件了. 简单解释下各个文件的作用: README.md 是默认首页文件,相当于网站的首页 index.html ,一般是介绍文字或相关导航链接. SUMMARY.md 是默认概括文件,主要是根据该文件内容生成相应的目录结构,同 README.md 一样都是被gitbook init 初始化默认创建的重要文件. _book 是默认的输出目录,存放着原始 markdown 渲染完毕后的 html 文件,可以直接打包到服务器充当静态网站使用.一般是执行 gitbook build 或 gitbook serve 自动生成的. book.json 是配置文件,用于个性化调整 gitbook 的相关配置,如定义电子书的标题,封面,作者等信息.虽然是手动创建但一般是必选的. GLOSSARY.md 是默认的词汇表,主要说明专业词汇的详细解释,这样阅读到专业词汇时就会有相应提示信息,也是手动创建但是可选的. LANGS.md 是默认的语言文件,用于国际化版本翻译,和 GLOSSARY.md 一样是手动创建但是可选的. README.md 首页文件[必须] 编辑 README.md 文件,随便写点内容并启动本地服务(gitbook serve)实时预览效果. SUMMARY.md 概括文件[必须] 先停止本地服务,编辑章节目录结构,然后重新再初始化(gitbook init)自动创建相应目录. _book 输出目录[可选] 执行 gitbook build 或 gitbook serve 命令后会自动生成静态网页. # 构建电子书 $ gitbook build info: 7 plugins are installed info: 6 explicitly listed info: loading plugin "highlight"... OK info: loading plugin "search"... OK info: loading plugin "lunr"... OK info: loading plugin "sharing"... OK info: loading plugin "fontsettings"... OK info: loading plugin "theme-default"... OK info: found 5 pages info: found 0 asset files info: >> generation finished with success in 0.7s ! # 查看输出目录 $ tree _book/ _book/ ├── first │ ├── 01.html │ └── 02.html ├── first.html ├── gitbook │ ├── fonts │ │ └── fontawesome │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── gitbook-plugin-fontsettings │ │ ├── fontsettings.js │ │ └── website.css │ ├── gitbook-plugin-highlight │ │ ├── ebook.css │ │ └── website.css │ ├── gitbook-plugin-lunr │ │ ├── lunr.min.js │ │ └── search-lunr.js │ ├── gitbook-plugin-search │ │ ├── lunr.min.js │ │ ├── search-engine.js │ │ ├── search.css │ │ └── search.js │ ├── gitbook-plugin-sharing │ │ └── buttons.js │ ├── gitbook.js │ ├── images │ │ ├── apple-touch-icon-precomposed-152.png │ │ └── favicon.ico │ ├── style.css │ └── theme.js ├── index.html ├── search_index.json └── second.html 10 directories, 28 files $ book.json 配置文件[可选] 在根目录下新建 book.json 配置文件,完整的支持项请参考官方文档,下面仅列举常用的一些配置项. title 标题 书籍的标题 示例: "title": "雪之梦技术驿站" author 作者 书籍的作者 示例: "author": "snowdreams1006" description 描述 书籍的简要描述 示例: "description": "雪之梦技术驿站又名snowdreams1006的技术小屋.主要分享个人的学习经验,一家之言,仅供参考." isbn 国际标准书号 书籍的国际标准书号 示例: "isbn": "978-0-13-601970-1" 选填,请参考 ISBN Search language 语言 支持语言项: 默认英语(en),设置成简体中文(zh-hans) en, ar, bn, cs, de, en, es, fa, fi, fr, he, it, ja, ko, no, pl, pt, ro, ru, sv, uk, vi, zh-hans, zh-tw 示例: "language": "zh-hans" direction 阅读顺序 阅读顺序,支持从右到左(rtl)或从左到右(ltr),默认值取决于语言值. 示例: "direction" : "ltr" gitbook 版本 指定 gitbook 版本,支持SemVer规范,接受类似于 >=3.2.3 的条件. 示例: "gitbook": "3.2.3" root 根目录 指定存放 gitbook 文件(除了book.json文件本身)的根目录 示例: "root": "." links 侧边栏链接 左侧导航栏添加链接,支持外链 示例; "links": { "sidebar": { "我的网站": "https://snowdreams1006.cn/" } } styles 自定义样式 自定义全局样式 示例: "styles": { "website": "styles/website.css", "ebook": "styles/ebook.css", "pdf": "styles/pdf.css", "mobi": "styles/mobi.css", "epub": "styles/epub.css" } plugins 插件 配置额外的插件列表,添加新插件项后需要运行 gitbook install 安装到当前项目. gitbook 默认自带5个插件,分别是: highlight 语法高亮插件 search 搜索插件 sharing 分享插件 font-settings 字体设置插件 livereload 热加载插件 后续会介绍一些常用插件,如需获取更多插件请访问官网插件市场 示例: "plugins": [ "github", "pageview-count", "mermaid-gb3", "-lunr", "-search", "search-plus", "splitter", "-sharing", "sharing-plus", "expandable-chapters-small", "anchor-navigation-ex", "edit-link", "copy-code-button", "chart", "favicon-plus", "donate" ] pluginsConfig 插件配置 安装插件的相应配置项,具体有哪些配置项是由插件本身提供的,应访问插件官网进行查询. "pluginsConfig": { "github": { "url": "https://github.com/snowdreams1006/snowdreams1006.github.io" }, "sharing": { "douban": true, "facebook": false, "google": false, "hatenaBookmark": false, "instapaper": false, "line": false, "linkedin": false, "messenger": false, "pocket": false, "qq": true, "qzone": true, "stumbleupon": false, "twitter": false, "viber": false, "vk": false, "weibo": true, "whatsapp": false, "all": [ "facebook", "google", "twitter", "weibo", "instapaper", "linkedin", "pocket", "stumbleupon" ] }, "edit-link": { "base": "https://github.com/snowdreams1006/snowdreams1006.github.io/blob/master", "label": "编辑本页" }, "chart": { "type": "c3" }, "favicon": "/images/favicon.ico", "appleTouchIconPrecomposed152": "/images/apple-touch-icon-precomposed-152.png", "output": "_book", "donate": { "wechat": "/images/wechat.jpg", "alipay": "/images/alipay.jpg", "title": "赏", "button": "捐赠", "alipayText": "支付宝", "wechatText": "微信" } } structure 目录结构配置 指定README.md,SUMMARY.md,GLOSSARY.md 和 LANGS.md 文件名称. 配置项 描述 structure.readme readme 文件名(默认值是 README.md) structure.summary summary 文件名(默认值是 SUMMARY.md) structure.glossary glossary 文件名(默认值是 GLOSSARY.md) structure.languages languages 文件名(默认值是 LANGS.md) pdf 配置 定制 pdf 输出格式,可能需要安装 ebook-convert 等相关插件 配置项 描述 pdf.pageNumbers 添加页码(默认值是 true ) pdf.fontSize 字体大小(默认值是 12 ) pdf.fontFamily 字体集(默认值是 Arial ) pdf.paperSize 页面尺寸(默认值是 a4 ),支持a0,a1,a2,a3,a4,a5,a6,b0,b1,b2,b3,b4,b5,b6,legal,letter pdf.margin.top 上边界(默认值是 56 ) pdf.margin.bottom 下边界(默认值是 56 ) pdf.margin.left 左边界(默认值是 62 ) pdf.margin.right 右边界(默认值是 62 ) 电子书封面照片 cover.jpg 和 cover_small.jpg,后续会详细说明. GLOSSARY.md 词汇表文件[可选] 词汇表文件,用于全书的专业词汇解释说明,比如鼠标悬停在专业词汇上会有相应提示. 语法格式: ## + + 专业词汇 学习 gitbook 前最好先学习下markdown和git,你知道他们的用途吗? 示例: ## markdown 简洁优雅的排版语言,简化版的 `HTML`,加强版的 `TXT`,详情请参考 [https://snowdreams1006.github.io/markdown/](https://snowdreams1006.github.io/markdown/) ## git 分布式版本控制系统,详情请参考 [https://snowdreams1006.github.io/git/](https://snowdreams1006.github.io/git/) LANGS.md 语言文件[可选] 支持国际化编写图书,一种语言一个单独子目录,同样地,将语言文件放到根目录下. 示例: * [English](en/) * [French](fr/) * [Español](es/) 章节小结 开发初始阶段运行 gitbook init 命令按照 SUMMARY.md 文件内容自动创建对应目录结构,编写各自文件内容后运行 gitbook serve 启动本地服务实时预览效果. 开发到一定程度后打算发布服务,再运行 gitbook build 输出到 _book/ 目录,别忘了配置 book.json 文件,然后就可以将 _book/ 文件夹整个扔到 nginx 等静态服务器上,这样就能联网访问你的电子书了. 是不是很简单,后续还会有如何发布与导出等相关教程,今天先到这里,下次见!

优秀的个人博客,低调大师

nodejs 开发企业微信第三方应用入门教程

最近公司要开发企业微信端的 Worktile,以前做的是企业微信内部应用,所以只适用于私有部署客户,而对于公有云客户就无法使用。所有本文就准备开发企业微信的第三方应用,主要介绍在调研阶段遇到的山珍海味。 开发之前你需要前注册为第三方服务商,然后用第三方服务商的账号创建应用,创建之后只需要管理员授权应用,第三方服务商即可为用户提供服务。 一、注册第三发服务商 登陆服务商官网,注册成为服务商,并登陆服务商管理后台。 二、配置开发信息 在创建应用之前,首先要配置好通用开发参数 在填写系统事件接收 url 时,要正确响应企业微信验证 url 的请求。这个可以参考企业微信后台,自建应用的接收消息的 api 设置。 在企业的管理端后台,进入需要设置接收消息的目标应用,点击“接收消息”的“设置API接收”按钮,进入配置页面。 要求填写应用的 URL、Token、EncodingAESKey 三个参数 URL 是企业后台接收企业微信推送请求的访问协议和地址,支持 http 或 https 协议(为了提高安全性,建议使用 https)。 Token 可由企业任意填写,用于生成签名。 EncodingAESKey 用于消息体的加密,是 AES 密钥的 Base64 编码。 2.1 验证 url 有效性 当点击保存的时候,企业微信会发生一条 get 请求到填写的 url 比如 url 设置的是https://api.worktile.com, 企业微信将发送如下验证请求: 请求地址:https://api.worktile.com/?msg_signature=ASDFQWEXZCVAQFASDFASDFSS×tamp=13500001234&nonce=123412323&echostr=ENCRYPT_STR 参数 说明 msg_signature 企业微信加密签名,msg_signature 结合了企业填写的 token、请求中的 timestamp、nonce 参数、加密的消息体 timestamp 时间戳 nonce 随机数 echostr 加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、receiveid 四个字段,其中 msg 即为消息内容明文 2.1.1 通过参数 msg_signature 对请求进行校验 首先要把刚才配置时随机生成的 token, timestamp, nonce, msg_encrypt 进行 sha1 加密,这里我们可以直接使用 npm 模块 sha1 进行加密,然后判断得到的 str 是否和 msg_signature 相等。 function sha1(str) { const md5sum = crypto.createHash('sha1'); md5sum.update(str); const ciphertext = md5sum.digest('hex'); return ciphertext; } function checkSignature(req, res, encrypt) { const query = req.query; console.log('Request URL: ', req.url); const signature = query.msg_signature; const timestamp = query.timestamp; const nonce = query.nonce; let echostr; console.log('encrypt', encrypt); if (!encrypt) { echostr = query.echostr; } else { echostr = encrypt; } console.log('timestamp: ', timestamp); console.log('nonce: ', nonce); console.log('signature: ', signature); // 将 token/timestamp/nonce 三个参数进行字典序排序 const tmpArr = [token, timestamp, nonce, echostr]; const tmpStr = sha1(tmpArr.sort().join('')); console.log('Sha1 String: ', tmpStr); // 验证排序并加密后的字符串与 signature 是否相等 if (tmpStr === signature) { // 原样返回echostr参数内容 const result = _decode(echostr); console.log('last', result); console.log('Check Success'); return result; } else { console.log('Check Failed'); return 'failed'; } } 2.1.2 解密 echostr 得到 msg 并返回 密文解密过程: 对刚才生成的 AESKey 进行 base64 解码 const EncodingAESKey = '21IpFqj8qolJbaqPqe1rVTAK5sgkaQ3GQmUKiUQLwRe'; let aesKey = Buffer.from(EncodingAESKey + '=', 'base64'); 对 AESKey 进行 aes-256-cbc 解密 function _decode(data) { let aesKey = Buffer.from('21IpFqj8qolJbaqPqe1rVTAK5sgkaQ3GQmUKiUQLwRe' + '=', 'base64'); let aesCipher = crypto.createDecipheriv("aes-256-cbc", aesKey, aesKey.slice(0, 16)); aesCipher.setAutoPadding(false); let decipheredBuff = Buffer.concat([aesCipher.update(data, 'base64'), aesCipher.final()]); decipheredBuff = PKCS7Decoder(decipheredBuff); let len_netOrder_corpid = decipheredBuff.slice(16); let msg_len = len_netOrder_corpid.slice(0, 4).readUInt32BE(0); const result = len_netOrder_corpid.slice(4, msg_len + 4).toString(); return result; // 返回一个解密后的明文- } function PKCS7Decoder (buff) { var pad = buff[buff.length - 1]; if (pad < 1 || pad > 32) { pad = 0; } return buff.slice(0, buff.length - pad); } 然后返回 result 即可 res.end(result); 2.2 回调 url 验证失败问题 验证 URL 时,经常会碰到 URL 验证失败的问题,解决思路是借助微信企业号接口调试工具 三、创建应用 四、测试应用 应用创建成功后,服务商可以授权 10 个测试企业 从企业微信应用市场发起授权时,企业微信给刚才应用设置的指令回调 url 发送一个 post 请求,比如: https://api.worktile.com/worktile?msg_signature=b99605616153ffbfbe6ebbb500bd211e67ed714d&timestamp=1551076894&nonce=1551709703,直接返回成功即可。 各个事件的回调,服务商在收到推送后都必须直接返回字符串 “success”,若返回值不是 “success”,企业微信会把返回内容当作错误信息。 app.post('/worktile', function (req, res) { console.log('req.body', req.body); res.send('success'); }); 测试应用注意事项 用于安装测试的企业微信帐号需服务商自行注册,每个应用支持同时添加 10 个测试企业微信账号 安装测试的企业微信帐号使用的是当前的应用配置信息,后续的修改不会进行同步;如需更新应用信息请重新授权安装 同一企业微信帐号,不支持同时安装测试应用和正式发布的应用 五、应用上线 已认证企业微信的服务商,可进入应用管理—点击提交上线—勾选应用—提交上线。 六、用户网页授权登录 6.1 构造第三方应用网页授权链接 如果第三方应用需要在打开的网页里面携带用户的身份信息,第一步需要构造如下的链接来获取 code: https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 参数 必须 说明 appid 是 第三方应用 id(即 ww 或 wx 开头的 suite_id)。注意与企业的网页授权登录不同 redirect_uri 是 授权后重定向的回调链接地址,请使用 urlencode 对链接进行处理 ,注意域名需要设置为第三方应用的可信域名 response_type 是 返回类型,此时固定为:code scope 是 应用授权作用域。snsapi_base:静默授权,可获取成员的基础信息(UserId与DeviceId);snsapi_userinfo:静默授权,可获取成员的详细信息,但不包含手机、邮箱等敏感信息;snsapi_privateinfo:手动授权,可获取成员的详细信息,包含手机、邮箱等敏感信息。 state 否 重定向后会带上 state 参数,企业可以填写 a-zA-Z0-9 的参数值,长度不可超过 128 个字节 #wechat_redirect 是 终端使用此参数判断是否需要带上身份信息 企业员工点击后,页面将跳转至 redirect_uri?code=CODE&state=STATE,第三方应用可根据 code 参数获得企业员工的 corpid 与 userid。code 长度最大为 512 字节。 6.2 获取访问用户身份 请求方式:GET(HTTPS) 请求地址:https://qyapi.weixin.qq.com/cgi-bin/service/getuserinfo3rd?access_token=SUITE_ACCESS_TOKEN&code=CODE 参数 必须 说明 access_token 是 第三方应用的 suite_access_token,参见“获取第三方应用凭证” code 是 通过成员授权获取到的 code,最大为 512 字节。每次成员授权带上的 code 将不一样,code 只能使用一次,5 分钟未被使用自动过期。 6.2.1 获取第三方应用的 suite_access_token 请求方式:POST(HTTPS) 请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token 参数 是否必须 说明 suite_id 是 以 ww 或 wx 开头应用 id(对应于旧的以 tj 开头的套件 id) suite_secret 是 应用 secret suite_ticket 是 企业微信后台推送的 ticket 由于第三方服务商可能托管了大量的企业,其安全问题造成的影响会更加严重,故 API 中除了合法来源 IP 校验之外,还额外增加了 suite_ticket 作为安全凭证。 获取 suite_access_token 时,需要 suite_ticket 参数。suite_ticket 由企业微信后台定时推送给“指令回调 URL”,每十分钟更新一次,见推送 suite_ticket。 suite_ticket 实际有效期为 30 分钟,可以容错连续两次获取 suite_ticket 失败的情况,但是请永远使用最新接收到的 suite_ticket。 通过本接口获取的 suite_access_token 有效期为 2 小时,开发者需要进行缓存,不可频繁获取。 6.2.2 获取推送 suite_ticket 企业微信服务器会定时(每十分钟)推送 ticket。ticket 会实时变更,并用于后续接口的调用。 请求方式:POST(HTTPS) 请求地址:https://api.ninesix.cc/worktile?msg_signature=87276aaf15a13e1eb2ebb6d93732ca668c3ddef8&timestamp=1551850300&nonce=1551051655 在发生授权、通讯录变更、ticket 变化等事件时,企业微信服务器会向应用的“指令回调 URL”推送相应的事件消息,nodejs 接收到的是 xml,解析后拿到 encrypt 字段,然后使用上面配置通用开发参数的 url 时用的解密方式,就可以得到 suite_ticket。 6.3 获取用户敏感信息 请求方式:POST(HTTPS) 请求地址:https://qyapi.weixin.qq.com/cgi-bin/service/getuserdetail3rd?access_token=SUITE_ACCESS_TOKEN { "user_ticket": "USER_TICKET" } 参数 必须 说明 access_token 是 第三方应用的 suite_access_token,参见“获取第三方应用凭证” user_ticket 是 成员票据 返回结果: { "errcode": 0, "errmsg": "ok", "corpid": "wwxxxxxxyyyyy", "userid": "lisi", "name": "李四", "mobile": "15913215421", "gender": "1", "email": "xxx@xx.com", "avatar": "http://shp.qpic.cn/bizmp/xxxxxxxxxxx/0", "qr_code": "https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=vcfc13b01dfs78e981c" } 七、用户授权成功 首页 详情页 八、给用户发消息 我们可以给推送文本、图片、视频、文件、图文等类型。 请求方式:POST(HTTPS) 请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN 推送的时候需要 access_token 和 应用的 agentId,第三方服务商,可通过接口 获取企业授权信息 获取该参数值,其实可以直接通过 获取企业永久授权码直接取到这两个值。 在我们测试安装应用成功之后,企业微信会 post 一条请求给指令回调 URL,通过上面的解密方式,可以解析到 xml 中的 auth_code 然后通过https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=SUITE_ACCESS_TOKEN和 auth_code 可以获取到 access_token 和 agentId,返回的 agent 是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个 agent。 再通过 access_token 和 agentId 就可以愉快的给用户发送消息了。 当点击链接时,可以跳到指定任务或者日程等,只不过返回时还是在企业微信的消息模块,并不能自动打开第三方应用,客服回复不支持这么做。 九、注意事项 api 可能有时效性,如有差异,以 官方 api 为准。 完整 demo 本文作者:王鹏 文章来源:Worktile技术博客 欢迎访问交流更多关于技术及协作的问题。 文章转载请注明出处。

优秀的个人博客,低调大师

超详细:自动化运维之jumpserver堡垒机入门到掌握

测试推荐环境 CPU: 64位双核处理器 内存: 4G DDR3 数据库:mysql 版本大于等于 5.6 mariadb 版本大于等于 5.5.6 环境 系统: CentOS 7 IP: 192.168.0.230 设置 selinux 和防火墙 [root@web1 ~]# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:0c:29:c5:33:97 brd ff:ff:ff:ff:ff:ff inet 192.168.0.230/24 brd 192.168.0.255 scope global noprefixroute dynamic ens33 valid_lft 4169sec preferred_lft 4169sec inet6 fe80::20c:29ff:fec5:3397/64 scope link valid_lft forever preferred_lft forever [root@web1 ~]# iptables -F [root@web1 ~]# iptables -X [root@web1 ~]# iptables -Z [root@web1 ~]# getenforce Disabled [root@web1 ~]# # 修改字符集, 否则可能报 input/output error的问题, 因为日志里打印了中文 [root@web1 ~]# localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 [root@web1 ~]# export LC_ALL=zh_CN.UTF-8 [root@web1 ~]# echo 'LANG="zh_CN.UTF-8"' > /etc/locale.conf 一. 准备 Python3 和 Python 虚拟环境 1.1 安装依赖包 [root@web1 ~]# yum -y install wget gcc epel-release git 1.2 安装 Python3.6 [root@web1 ~]# yum -y install python36 python36-devel # 如果下载速度很慢, 可以换国内源 [root@web1 ~]# wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo [root@web1 ~]# yum -y install python36 python36-devel 1.3 建立 Python 虚拟环境因为 CentOS 7 自带的是 Python2, 而 Yum 等工具依赖原来的 Python, 为了不扰乱原来的环境我们来使用 Python 虚拟环境 [root@web1 ~]# cd /opt [root@web1 opt]# python3.6 -m venv py3 [root@web1 opt]# source /opt/py3/bin/activate # 看到下面的提示符代表成功, 以后运行 Jumpserver 都要先运行以上 source 命令, 以下所有命令均在该虚拟环境中运行(py3) [root@localhost py3] 二. 安装 Jumpserver 2.1 下载或 Clone 项目 项目提交较多 git clone 时较大, 你可以选择去 Github 项目页面直接下载zip包。 [root@web1 opt ]# cd /opt/ [root@web1 opt ]# git clone https://github.com/jumpserver/jumpserver.git 2.2 安装依赖 RPM 包 [root@web1 ~]# cd /opt/jumpserver/requirements [root@web1 requirements ]# yum -y install $(cat rpm_requirements.txt) # 如果没有任何报错请继续 2.3 安装 Python 库依赖 [root@web1 requirements ]# pip install --upgrade pip setuptools [root@web1 requirements ]# pip install -r requirements.txt 或者 # 如果下载速度很慢, 可以换国内源 [root@web1 requirements ]# pip install --upgrade pip setuptools -i https://mirrors.aliyun.com/pypi/simple/ [root@web1 requirements ]# pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ 2.4 安装 Redis, Jumpserver 使用 Redis 做 cache 和 celery broke [root@web1 requirements ]# yum -y install redis [root@web1 requirements ]# systemctl enable redis [root@web1 requirements ]# systemctl start redis 2.5 安装 MySQL 本教程使用 Mysql 作为数据库, 如果不使用 Mysql 可以跳过相关 Mysql 安装和配置 [root@web1 requirements ]# yum -y install mariadb mariadb-devel mariadb-server # centos7下安装的是mariadb [root@web1 requirements ]# systemctl enable mariadb [root@web1 requirements ]# systemctl start mariadb 2.6 创建数据库 Jumpserver 并授权 [root@web1 requirements ]# DB_PASSWORD=`cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 24` # 生成随机数据库密码 [root@web1 requirements ]# echo -e "\033[31m 你的数据库密码是 $DB_PASSWORD \033[0m" [root@web1 requirements ]# mysql -uroot -e "create database jumpserver default charset 'utf8'; grant all on jumpserver.* to 'jumpserver'@'127.0.0.1' identified by '$DB_PASSWORD'; flush privileges;" 2.7 修改 Jumpserver 配置文件 [root@web1 requirements ]# cd /opt/jumpserver [root@web1 jumpserver]# cp config_example.yml config.yml [root@web1 jumpserver]# SECRET_KEY=`cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 50` # 生成随机SECRET_KEY [root@web1 jumpserver]# BOOTSTRAP_TOKEN=`cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 16` # 生成随机BOOTSTRAP_TOKEN [root@web1 jumpserver]# sed -i "s/SECRET_KEY:/SECRET_KEY: $SECRET_KEY/g" /opt/jumpserver/config.yml [root@web1 jumpserver]# sed -i "s/BOOTSTRAP_TOKEN:/BOOTSTRAP_TOKEN: $BOOTSTRAP_TOKEN/g" /opt/jumpserver/config.yml [root@web1 jumpserver]# sed -i "s/# DEBUG: true/DEBUG: false/g" /opt/jumpserver/config.yml [root@web1 jumpserver]# sed -i "s/# LOG_LEVEL: DEBUG/LOG_LEVEL: ERROR/g" /opt/jumpserver/config.yml [root@web1 jumpserver]# sed -i "s/# SESSION_EXPIRE_AT_BROWSER_CLOSE: false/SESSION_EXPIRE_AT_BROWSER_CLOSE: true/g" /opt/jumpserver/config.yml [root@web1 jumpserver]# sed -i "s/DB_PASSWORD: /DB_PASSWORD: $DB_PASSWORD/g" /opt/jumpserver/config.yml [root@web1 jumpserver]# echo -e "\033[31m 你的SECRET_KEY是 $SECRET_KEY \033[0m" [root@web1 jumpserver]# echo -e "\033[31m 你的BOOTSTRAP_TOKEN是 $BOOTSTRAP_TOKEN \033[0m" [root@web1 jumpserver]# cat config.yml # 确认内容有没有错误 # SECURITY WARNING: keep the secret key used in production secret! # 加密秘钥 生产环境中请修改为随机字符串,请勿外泄, 可使用命令生成 # $ cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 49;echo SECRET_KEY: 7hB9ogGBpfqqW3uqg5l8vq49FBbOKXESDaWfjvI5hdYBytfjXt # SECURITY WARNING: keep the bootstrap token used in production secret! # 预共享Token coco和guacamole用来注册服务账号,不在使用原来的注册接受机制 BOOTSTRAP_TOKEN: IAvMqFnKUvmjjlLb # Development env open this, when error occur display the full process track, Production disable it # DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志 DEBUG: false # DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/ # 日志级别 LOG_LEVEL: ERROR # LOG_DIR: # Session expiration setting, Default 24 hour, Also set expired on on browser close # 浏览器Session过期时间,默认24小时, 也可以设置浏览器关闭则过期 # SESSION_COOKIE_AGE: 86400 SESSION_EXPIRE_AT_BROWSER_CLOSE: true # Database setting, Support sqlite3, mysql, postgres .... # 数据库设置 # See https://docs.djangoproject.com/en/1.10/ref/settings/#databases # SQLite setting: # 使用单文件sqlite数据库 # DB_ENGINE: sqlite3 # DB_NAME: # MySQL or postgres setting like: # 使用Mysql作为数据库 DB_ENGINE: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_USER: jumpserver DB_PASSWORD: cK2VQVUV16Q8F2U41flDfAFb DB_NAME: jumpserver # When Django start it will bind this host and port # ./manage.py runserver 127.0.0.1:8080 # 运行时绑定端口 HTTP_BIND_HOST: 0.0.0.0 HTTP_LISTEN_PORT: 8080 # Use Redis as broker for celery and web socket # Redis配置 REDIS_HOST: 127.0.0.1 REDIS_PORT: 6379 # REDIS_PASSWORD: # REDIS_DB_CELERY: 3 # REDIS_DB_CACHE: 4 # Use OpenID authorization # 使用OpenID 来进行认证设置 # BASE_SITE_URL: http://localhost:8080 # AUTH_OPENID: false # True or False # AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/ # AUTH_OPENID_REALM_NAME: realm-name # AUTH_OPENID_CLIENT_ID: client-id # AUTH_OPENID_CLIENT_SECRET: client-secret # OTP settings # OTP/MFA 配置 # OTP_VALID_WINDOW: 0 # OTP_ISSUER_NAME: Jumpserver 2.8 运行 Jumpserver [root@web1 jumpserver]# ./jms start all -d # 后台运行使用 -d 参数./jms start all -d # 新版本更新了运行脚本, 使用方式./jms start|stop|status|restart all 后台运行请添加 -d 参数 如果运行不报错, 请继续往下操作 三. 安装 SSH Server 和 WebSocket Server: Coco 3.1 下载或 Clone 项目 [root@web1 jumpserver]# cd /opt [root@web1 opt]# source /opt/py3/bin/activate [root@web1 opt]# git clone https://github.com/jumpserver/coco.git 3.2 安装依赖 [root@web1 opt]# cd /opt/coco/requirements [root@web1 requirements]# yum -y install $(cat rpm_requirements.txt) [root@web1 requirements]# pip install -r requirements.txt # 如果下载速度很慢, 可以换国内源 [root@web1 requirements]# pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ 3.3 修改配置文件并运行 [root@web1 requirements]# cd /opt/coco [root@web1 coco]# cp config_example.yml config.yml [root@web1 coco]# sed -i "s/BOOTSTRAP_TOKEN: <PleasgeChangeSameWithJumpserver>/BOOTSTRAP_TOKEN: $BOOTSTRAP_TOKEN/g" /opt/coco/config.yml [root@web1 coco]# sed -i "s/# LOG_LEVEL: INFO/LOG_LEVEL: ERROR/g" /opt/coco/config.yml [root@web1 coco]# cat config.yml # 项目名称, 会用来向Jumpserver注册, 识别而已, 不能重复 # NAME: {{ Hostname }} # Jumpserver项目的url, api请求注册会使用 CORE_HOST: http://127.0.0.1:8080 # Bootstrap Token, 预共享秘钥, 用来注册coco使用的service account和terminal # 请和jumpserver 配置文件中保持一致,注册完成后可以删除 BOOTSTRAP_TOKEN: IAvMqFnKUvmjjlLb # 启动时绑定的ip, 默认 0.0.0.0 # BIND_HOST: 0.0.0.0 # 监听的SSH端口号, 默认2222 # SSHD_PORT: 2222 # 监听的HTTP/WS端口号,默认5000 # HTTPD_PORT: 5000 # 项目使用的ACCESS KEY, 默认会注册,并保存到 ACCESS_KEY_STORE中, # 如果有需求, 可以写到配置文件中, 格式 access_key_id:access_key_secret # ACCESS_KEY: null # ACCESS KEY 保存的地址, 默认注册后会保存到该文件中 # ACCESS_KEY_FILE: data/keys/.access_key # 加密密钥 # SECRET_KEY: null # 设置日志级别 [DEBUG, INFO, WARN, ERROR, FATAL, CRITICAL] LOG_LEVEL: ERROR # 日志存放的目录 # LOG_DIR: logs # SSH白名单 # ALLOW_SSH_USER: all # SSH黑名单, 如果用户同时在白名单和黑名单,黑名单优先生效 # BLOCK_SSH_USER: # - # 和Jumpserver 保持心跳时间间隔 # HEARTBEAT_INTERVAL: 5 # Admin的名字,出问题会提示给用户 # ADMINS: '' # SSH连接超时时间 (default 15 seconds) # SSH_TIMEOUT: 30 # 语言 [en,zh] # LANGUAGE_CODE: zh # SFTP的根目录, 可选 /tmp, Home其他自定义目录 # SFTP_ROOT: /server/jms_sftp # SFTP是否显示隐藏文件 # SFTP_SHOW_HIDDEN_FILE: false [root@web1 coco]# source /opt/py3/bin/activate (py3) [root@web1 coco]# ./cocod start -d # 后台运行使用 -d 参数./cocod start -d Use eventlet dispatch Stop coco process Start coco process (py3) [root@web1 coco]# # 新版本更新了运行脚本, 使用方式./cocod start|stop|status|restart 后台运行请添加 -d 参数 四. 安装 Web Terminal 前端: Luna Luna 已改为纯前端, 需要 Nginx 来运行访问访问(https://github.com/jumpserver/luna/releases)下载对应版本的 release 包, 直接解压不需要编译4.1 解压 Luna (py3) [root@web1 coco]# cd /opt/ (py3) [root@web1 opt]# wget https://github.com/jumpserver/luna/releases/download/1.4.8/luna.tar.gz (py3) [root@web1 opt]# tar xf luna.tar.gz (py3) [root@web1 opt]# chown -R root:root luna 五. 安装 Windows 支持组件(如果不需要管理 windows 资产, 可以直接跳过这一步) 5.1 安装依赖 (py3) [root@web1 opt]# mkdir /usr/local/lib/freerdp/ (py3) [root@web1 opt]# ln -s /usr/local/lib/freerdp /usr/lib64/freerdp (py3) [root@web1 opt]# rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro (py3) [root@web1 opt]# rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm (py3) [root@web1 opt]# yum -y localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-7.noarch.rpm (py3) [root@web1 opt]# yum install -y java-1.8.0-openjdk libtool (py3) [root@web1 opt]# yum install -y cairo-devel libjpeg-turbo-devel libpng-devel uuid-devel (py3) [root@web1 opt]# yum install -y ffmpeg-devel freerdp-devel freerdp-plugins pango-devel libssh2-devel libtelnet-devel libvncserver-devel pulseaudio-libs-devel openssl-devel libvorbis-devel libwebp-devel ghostscript 5.2 编译安装 guacamole 服务 (py3) [root@web1 opt]# cd /opt (py3) [root@web1 opt]# git clone https://github.com/jumpserver/docker-guacamole.git (py3) [root@web1 opt]# cd /opt/docker-guacamole/ (py3) [root@web1 docker-guacamole ]# tar -xf guacamole-server-0.9.14.tar.gz (py3) [root@web1 docker-guacamole ]# cd guacamole-server-0.9.14 (py3) [root@web1 guacamole-server-0.9.14 ]# autoreconf -fi (py3) [root@web1 guacamole-server-0.9.14 ]# ./configure --with-init-dir=/etc/init.d (py3) [root@web1 guacamole-server-0.9.14 ]# make && make install (py3) [root@web1 guacamole-server-0.9.14 ]# cd .. (py3) [root@web1 docker-guacamole ]# rm -rf guacamole-server-0.9.14 (py3) [root@web1 docker-guacamole ]# ldconfig 5.3 配置 Tomcat (py3) [root@web1 docker-guacamole ]# mkdir -p /config/guacamole /config/guacamole/lib /config/guacamole/extensions # 创建 guacamole 目录 (py3) [root@web1 docker-guacamole ]# ln -sf /opt/docker-guacamole/guacamole-auth-jumpserver-0.9.14.jar /config/guacamole/extensions/guacamole-auth-jumpserver-0.9.14.jar (py3) [root@web1 docker-guacamole ]# ln -sf /opt/docker-guacamole/root/app/guacamole/guacamole.properties /config/guacamole/guacamole.properties # guacamole 配置文件 (py3) [root@web1 docker-guacamole ]# cd /config (py3) [root@web1 config ]# wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-8/v8.5.38/bin/apache-tomcat-8.5.38.tar.gz (py3) [root@web1 config ]# tar xf apache-tomcat-8.5.38.tar.gz (py3) [root@web1 config ]# rm -rf apache-tomcat-8.5.38.tar.gz (py3) [root@web1 config ]# mv apache-tomcat-8.5.38 tomcat8 (py3) [root@web1 config ]# rm -rf /config/tomcat8/webapps/* (py3) [root@web1 config ]# ln -sf /opt/docker-guacamole/guacamole-0.9.14.war /config/tomcat8/webapps/ROOT.war # guacamole client (py3) [root@web1 config ]# sed -i 's/Connector port="8080"/Connector port="8081"/g' /config/tomcat8/conf/server.xml # 修改默认端口为 8081 (py3) [root@web1 config ]# sed -i 's/FINE/WARNING/g' /config/tomcat8/conf/logging.properties # 修改 log 等级为 WARNING (py3) [root@web1 config ]# wget https://github.com/ibuler/ssh-forward/releases/download/v0.0.5/linux-amd64.tar.gz (py3) [root@web1 config ]# tar xf linux-amd64.tar.gz -C /bin/ (py3) [root@web1 config ]# chmod +x /bin/ssh-forward 5.4 配置环境变量 (py3) [root@web1 config ]# export JUMPSERVER_SERVER=http://127.0.0.1:8080 # http://127.0.0.1:8080 指 jumpserver 访问地址 $ echo "export JUMPSERVER_SERVER=http://127.0.0.1:8080" >> ~/.bashrc # BOOTSTRAP_TOKEN 为 Jumpserver/config.yml 里面的 BOOTSTRAP_TOKEN (py3) [root@web1 config ]# export BOOTSTRAP_TOKEN=$BOOTSTRAP_TOKEN (py3) [root@web1 config ]# echo "export BOOTSTRAP_TOKEN=$BOOTSTRAP_TOKEN" >> ~/.bashrc (py3) [root@web1 config ]# export JUMPSERVER_KEY_DIR=/config/guacamole/keys (py3) [root@web1 config ]# echo "export JUMPSERVER_KEY_DIR=/config/guacamole/keys" >> ~/.bashrc (py3) [root@web1 config ]# export GUACAMOLE_HOME=/config/guacamole (py3) [root@web1 config ]# echo "export GUACAMOLE_HOME=/config/guacamole" >> ~/.bashrc 5.5 启动 Guacamole (py3) [root@web1 config ]# /etc/init.d/guacd start (py3) [root@web1 config ]# sh /config/tomcat8/bin/startup.sh 六. 配置 Nginx 整合各组件 6.1 安装 Nginx (py3) [root@web1 config ]# yum install yum-utils (py3) [root@web1 config ]# vi /etc/yum.repos.d/nginx.repo [nginx-stable]name=nginx stable repo baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck=1enabled=1gpgkey=https://nginx.org/keys/nginx_signing.key (py3) [root@web1 config ]# yum install -y nginx (py3) [root@web1 config ]# rm -rf /etc/nginx/conf.d/default.conf (py3) [root@web1 config ]# systemctl enable nginx 6.2 准备配置文件 修改 /etc/nginx/conf.d/jumpserver.conf (py3) [root@web1 config ]# vi /etc/nginx/conf.d/jumpserver.conf server { listen 80; # 代理端口, 以后将通过此端口进行访问, 不再通过8080端口 # server_name demo.jumpserver.org; # 修改成你的域名或者注释掉 client_max_body_size 100m; # 录像及文件上传大小限制 location /luna/ { try_files $uri / /index.html; alias /opt/luna/; # luna 路径, 如果修改安装目录, 此处需要修改 } location /media/ { add_header Content-Encoding gzip; root /opt/jumpserver/data/; # 录像位置, 如果修改安装目录, 此处需要修改 } location /static/ { root /opt/jumpserver/data/; # 静态资源, 如果修改安装目录, 此处需要修改 } location /socket.io/ { proxy_pass http://localhost:5000/socket.io/; # 如果coco安装在别的服务器, 请填写它的ip proxy_buffering off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; access_log off; } location /coco/ { proxy_pass http://localhost:5000/coco/; # 如果coco安装在别的服务器, 请填写它的ip proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; access_log off; } location /guacamole/ { proxy_pass http://localhost:8081/; # 如果guacamole安装在别的服务器, 请填写它的ip proxy_buffering off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; access_log off; } location / { proxy_pass http://localhost:8080; # 如果jumpserver安装在别的服务器, 请填写它的ip proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }} 6.3 运行 Nginx (py3) [root@web1 config ]# nginx -t # 确保配置没有问题, 有问题请先解决 # CentOS 7 (py3) [root@web1 config ]# systemctl start nginx (py3) [root@web1 config ]# systemctl enable nginx 6.4 开始使用 Jumpserver检查应用是否已经正常运行服务全部启动后, 访问http://192.168.0.230, 访问nginx代理的端口, 不要再通过8080端口访问默认账号: admin 密码: admin到Jumpserver 会话管理-终端管理 检查 Coco Guacamole 等应用的注册。 测试连接 如果登录客户端是 macOS 或 Linux, 登录语法如下 $ ssh -p2222 admin@192.168.244.144 $ sftp -P2222 admin@192.168.244.144 密码: admin 如果登录客户端是 Windows, Xshell Terminal 登录语法如下 $ ssh admin@192.168.244.144 2222 $ sftp admin@192.168.244.144 2222 密码: admin 如果能登陆代表部署成功 # sftp默认上传的位置在资产的 /tmp 目录下# windows拖拽上传的位置在资产的 Guacamole RDP上的 G 目录下 设置授权规则,重新连接 [root@web1 ~]# ssh leoheng@192.168.0.230 -p 2222 leoheng@192.168.0.230's password: leoheng, 欢迎使用Jumpserver开源跳板机系统 1) 输入 ID 直接登录 或 输入部分 IP,主机名,备注 进行搜索登录(如果唯一). 2) 输入 / + IP, 主机名 or 备注 搜索. 如: /ip 3) 输入 p 显示您有权限的主机. 4) 输入 g 显示您有权限的节点. 5) 输入 g + 节点ID 显示节点下主机. 如: g1 6) 输入 s 中/英文切换. 7) 输入 h 帮助. 0) 输入 r 刷新最新的机器和节点信息. 0) 输入 q 退出. Opt> p ID 主机名 IP 登录用户 备注 1 k8s 192.168.0.228 [leo] 页码: 1, 数量: 1, 总页数: 1, 总数量: 1 Opt> 1 开始连接到 leo@k8s 1.1 Last login: Fri Mar 8 15:11:05 2019 from 192.168.0.230 [leo@k8s ~]$ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:0c:29:1f:fd:1b brd ff:ff:ff:ff:ff:ff inet 192.168.0.228/24 brd 192.168.0.255 scope global noprefixroute dynamic ens33 valid_lft 6579sec preferred_lft 6579sec inet6 fe80::20c:29ff:fe1f:fd1b/64 scope link valid_lft forever preferred_lft forever 3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default link/ether 02:42:c5:84:a3:7c brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever [leo@k8s ~]$ 6.5 设置邮件服务 (py3) [root@web1 opt]# yum install -y mailx ##安装邮件服务 (py3) [root@web1 opt]# vim /etc/mail.rc ##在配置文件最后添加 set from=**@****.com #(需修改)收件人显示的发件人名称,可填写你的名字等 set smtp=smtp.ym.163.com (需修改)你所使用的外部邮箱的smtp服务器地址,这里使用163的邮件服务器 set smtp-auth-user=**@****.com (需修改)你所使用的外部邮箱的用户名 set smtp-auth-password=******* (需修改)你所使用的外部邮箱密码 set smtp-auth=login 测试邮件发送 (py3) [root@web1 opt]# echo 111 |mailx -v -s "test info" ******@qq.com Resolving host smtp.ym.163.com . . . done. Connecting to 59.111.176.15:smtp . . . connected. 220 proxy-sm2.ym.internal ESMTP ready >>> EHLO web1 250-proxy-sm2.ym.internal 250-PIPELINING 250-8BITMIME 250-AUTH=LOGIN PLAIN 250-AUTH PLAIN LOGIN 250 STARTTLS >>> AUTH LOGIN 334 VXNlcm5hbWU6 >>> a2JAYnV5ZXJjYW1wLmNvbQ== 334 UGFzc3dvcmQ6 >>> YWNiZDY1NDMyMQ== 235 2.0.0 OK >>> MAIL FROM:<**@******.com> 250 2.1.0 Ok >>> RCPT TO:<******@qq.com> 250 2.1.5 Ok >>> DATA 354 End data with <CR><LF>.<CR><LF> >>> . 250 2.0.0 Ok: queued as 77CE7B41A88 >>> QUIT 221 2.0.0 Bye 邮箱收件情况 设置jms的邮件发送 更多的博客转移到个人博客上了,请点击以下链接:个人博客

优秀的个人博客,低调大师

网络爬虫入门:你的第一个爬虫项目(requests库)

0.采用requests库 虽然urllib库应用也很广泛,而且作为Python自带的库无需安装,但是大部分的现在python爬虫都应用requests库来处理复杂的http请求。requests库语法上简洁明了,使用上简单易懂,而且正逐步成为大多数网络爬取的标准。 1. requests库的安装采用pip安装方式,在cmd界面输入: pip install requests 小编推荐一个学python的学习qun 491308659 验证码:南烛无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享 2. 示例代码我们将处理http请求的头部处理来简单进行反反爬虫处理,以及代理的参数设置,异常处理等。 import requests def download(url, num_retries=2, user_agent='wswp', proxies=None): '''下载一个指定的URL并返回网页内容 参数: url(str): URL 关键字参数: user_agent(str):用户代理(默认值:wswp) proxies(dict): 代理(字典): 键:‘http’'https' 值:字符串(‘http(s)://IP’) num_retries(int):如果有5xx错误就重试(默认:2) #5xx服务器错误,表示服务器无法完成明显有效的请求。 #https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81 ''' print('==========================================') print('Downloading:', url) headers = {'User-Agent': user_agent} #头部设置,默认头部有时候会被网页反扒而出错 try: resp = requests.get(url, headers=headers, proxies=proxies) #简单粗暴,.get(url) html = resp.text #获取网页内容,字符串形式 if resp.status_code >= 400: #异常处理,4xx客户端错误 返回None print('Download error:', resp.text) html = None if num_retries and 500 <= resp.status_code < 600: # 5类错误 return download(url, num_retries - 1)#如果有服务器错误就重试两次 except requests.exceptions.RequestException as e: #其他错误,正常报错 print('Download error:', e) html = None return html #返回html print(download('http://www.baidu.com')) 结果: Downloading: http://www.baidu.com <!DOCTYPE html> <!--STATUS OK--> </script> <script> if(navigator.cookieEnabled){ document.cookie="NOJS=;expires=Sat, 01 Jan 2000 00:00:00 GMT"; } </script> </body> </html>

优秀的个人博客,低调大师

[雪峰磁针石博客]python 3.7极速入门教程7互联网

本文教程目录 7互联网 互联网访问 urllib urllib是用于打开URL的Python模块。 import urllib.request # open a connection to a URL using urllib webUrl = urllib.request.urlopen('https://china-testing.github.io/address.html') #get the result code and print it print ("result code: " + str(webUrl.getcode())) # read the data from the URL and print it data = webUrl.read() print (data) json json是一种受JavaScript对象文字语法启发的轻量级数据交换格式。是目前互联网最流行的数据交换格式。 json公开了标准库marshal和pickle模块的用户所熟悉的API。 >>> import json >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) '["foo", {"bar": ["baz", null, 1.0, 2]}]' >>> print(json.dumps("\"foo\bar")) "\"foo\bar" >>> print(json.dumps('\u1234')) "\u1234" >>> print(json.dumps('\\')) "\\" >>> print(json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True)) {"a": 0, "b": 0, "c": 0} >>> from io import StringIO >>> io = StringIO() >>> json.dump(['streaming API'], io) >>> io.getvalue() '["streaming API"]' 参考资料 讨论qq群144081101 591302926 567351477 钉钉免费群21745728 本文最新版本地址 本文涉及的python测试开发库 谢谢点赞! XML XML即可扩展标记语言(eXtensible Markup Language)。它旨在存储和传输中小数据量,并广泛用于共享结构化信息。 import xml.dom.minidom doc = xml.dom.minidom.parse("test.xml"); # print out the document node and the name of the first child tag print (doc.nodeName) print (doc.firstChild.tagName) # get a list of XML tags from the document and print each one expertise = doc.getElementsByTagName("expertise") print ("{} expertise:".format(expertise.length)) for skill in expertise: print(skill.getAttribute("name")) # create a new XML tag and add it into the document newexpertise = doc.createElement("expertise") newexpertise.setAttribute("name", "BigData") doc.firstChild.appendChild(newexpertise) print (" ") expertise = doc.getElementsByTagName("expertise") print ("{} expertise:".format(expertise.length)) for skill in expertise: print (skill.getAttribute("name")) ElementTree是处理XML文件的简便方法。 import xml.dom.minidom import xml.etree.ElementTree as ET tree = ET.parse('items.xml') root = tree.getroot() # all items data print('Expertise Data:') for elem in root: for subelem in elem: print(subelem.text)

优秀的个人博客,低调大师

[雪峰磁针石博客]python 3.7极速入门教程2 Hello与变量

Hello 命令行方式 $ python Python 3.7.0 (default, Jun 28 2018, 13:15:42) [GCC 7.2.0] :: Anaconda, Inc. on linux Type "help", "copyright", "credits" or "license" for more information. >>> print("Hello, https://china-testing.github.io/") Hello, https://china-testing.github.io/ ipython方式 $ ipython Python 3.7.0 (default, Jun 28 2018, 13:15:42) Type 'copyright', 'credits' or 'license' for more information IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. In [1]: print("Hello, https://china-testing.github.io/") Hello, https://china-testing.github.io/ IDE方式 文件方式 上面代码地址:https://github.com/china-testing/python-api-tesing/blob/master/python3.7quick/2hello.py __main__ 当Python解释器读取源文件时,它将执行其中的所有代码。 当Python运行“源文件”作为主程序时,它将特殊变量( __name __)设置为具(“ __main __”)。 “if __name __ ==” __main __“允许您将Python文件作为可重用模块或独立程序运行。 与C一样,Python使用==进行比较,而使用=进行赋值。 上面代码地址:https://github.com/china-testing/python-api-tesing/blob/master/python3.7quick/2main.py 参考资料 讨论qq群144081101 591302926 567351477 钉钉免费群21745728 本文最新版本地址 本文涉及的python测试开发库 谢谢点赞! 本文相关海量书籍下载 https://linuxize.com/post/how-to-install-anaconda-on-ubuntu-18-04/ 变量 Python变量是用于存储值的保留内存位置。 换句话说,python程序中的变量将数据提供给计算机进行处理。 Python中的每个值都有数据类型。 Python中不同的数据类型是数值,列表,元组,字符串,字典等。变量可以用任何名称声明,甚至可以用a,aa,abc等字母表来声明,命名规则和C语言的类似。字母或下划线开头,除第一位外可以包含数字。 上面代码地址:https://github.com/china-testing/python-api-tesing/blob/master/python3.7quick/2var.py 作用域 如果要在程序或模块的其余部分使用相同的变量,可声明为全局变量;如果只在特定函数或方法中使用该变量,则使用局部变量。 让我们通过以下程序理解本地变量和全局变量之间的差异。 全局变量f被赋予值101 函数中声明局部变量,赋值"I am learning Python." 上面代码地址:https://github.com/china-testing/python-api-tesing/blob/master/python3.7quick/2local.py 关键字global,可以在函数内引用全局变量。 实际上局部找不到变量也会到全局去找。上面代码地址:https://github.com/china-testing/python-api-tesing/blob/master/python3.7quick/2global.py 使用命令del“variable name”可以删除变量。 在下面的例子中,我们删除了变量f,当我们继续打印它时,得到错误“变量名未定义”,这意味着你已经删除了变量。

优秀的个人博客,低调大师

Netty 入门与实战:仿写微信 IM 即时通讯系统

作为一个学 Java 的,如果没有研究过 Netty,那么你对 Java 语言的使用和理解仅仅停留在表面水平,如果你要进阶,想了解 Java 服务器的深层高阶知识,Netty 绝对是一个必须要过的门槛。 有了 Netty,你可以实现自己的 HTTP 服务器,FTP 服务器,UDP 服务器,RPC 服务器,WebSocket 服务器,Redis 的 Proxy 服务器,MySQL 的 Proxy 服务器等等。 如果你想知道Nginx是怎么写出来的,如果你想知道 Tomcat 和 Jetty 是如何实现的,如果你也想实现一个简单的 Redis 服务器,那都应该好好理解一下 Netty,它们高性能的原理都是类似的。 Netty 是互联网中间件领域使用最广泛最核心的网络通信框架。掌握它是作为一名初中级工程师迈向高级工程师最重要的技能之一,同时, Netty 也是中高级后端工程师技术面试中,面试官最喜欢问的问题之一。 然而,绝大部分工程师学习的 Netty 知识点都比较零散,不成系统,无法串成一条线。 于是,一位有情怀的架构师,某大型互联网公司基础架构部技术专家闪电侠(闪电侠Github 地址:github.com/lightningMa…),撰写了一本小册子,梳理了自己多年 Netty 实践经验,以帮助更多工程师更快,更轻松的了解 Netty 。 闪电侠所在的公司,使用 Netty 的长连集群数为几十规模,机器数为数百规模,线上 QPS 为几十万级别的规模,日吞吐为百亿规模,如此大的并发量,仅使用了 Netty 就能够轻松应对,而这些知识点在小册子中都会毫无保留得奉献给大家。 小册通过一个仿微信 IM 系统,来演示如何使用 Netty 一步一步进行服务端和客户端长连通信的开发,其中所涉及的代码将会按照小节的顺序放置到 Github 上,每小节对应一个分支,方便读者由浅入深地学习。 此本小册会通过控制台来进行用户操作的模拟,包括: ●客户端登录验证 ●客户端之间收发消息 ●群的创建 ●群聊成员管理 ●群内成员收发消息 ●客户端退出登陆 这本册子作者使用了大量的图来展示程序逻辑结构,这些图示直观易懂,相信广大工程师们学习 Netty 会更加轻松有趣。 原文发布时间为:2018-10-17 本文来自云栖社区合作伙伴“Java架构沉思录”,了解相关信息可以关注“Java架构沉思录”。

优秀的个人博客,低调大师

【Java入门提高篇】Day34 Java容器类详解(十五)WeakHashMap详解

在Java容器详解系列文章的最后,介绍一个相对特殊的成员:WeakHashMap,从名字可以看出它是一个Map。它的使用上跟HashMap并没有什么区别,所以很多地方这里就不做过多介绍了,可以翻看一下前面HashMap中的内容。本篇主要介绍它与HashMap的不同之处。 WeakHashMap特殊之处在于WeakHashMap里的entry可能会被垃圾回收器自动删除,也就是说即使你没有调用remove()或者clear()方法,它的entry也可能会慢慢变少。所以多次调用比如isEmpty,containsKey,size等方法时可能会返回不同的结果。 接下来希望能带着这么几个问题来进行阅读: 1、WeakHashMap中的Entry为什么会自动被回收。 2、WeakHashMap与HashMap的区别是什么。 3、WeakHashMap的引用场景有哪些。 WeakHashMap探秘 从说明可以看出,WeakHashMap的特殊之处便在于它的Entry与众不同,里面的Entry会被垃圾回收器自动回收,那么问题来了,为什么会被自动回收呢?HashMap里的Entry并不会被自动回收,除非把它从Map中移除掉。 其实这个秘密就在于弱引用,WeakHashMap中的key是间接保存在弱引用中的,所以当key没有被继续使用时,就可能会在GC的时候被回收掉。 只有key对象是使用弱引用保存的,value对象实际上仍旧是通过普通的强引用来保持的,所以应该确保value不会直接或者间接的保持其对应key的强引用,因为这样会阻止key被回收。 如果对于引用类型不熟悉的话,可以先阅读这篇文章。 下面来从源码角度看看具体是如何实现这个特性的。 继承结构 WeakHashMap并不是继承自HashMap,而是继承自AbstractMap,跟HashMap的继承结构差不多。 存储结构 WeakHashMap中的数据结构是数组+链表的形式,这一点跟HashMap也是一致的,但不同的是,在JDK8中,当发生较多key冲突的时候,HashMap中会由链表转为红黑树,而WeakHashMap则一直使用链表进行存储。 成员变量 // 默认初始容量,必须是2的幂 private static final int DEFAULT_INITIAL_CAPACITY = 16; // 最大容量 private static final int MAXIMUM_CAPACITY = 1 << 30; // 默认装载因子 private static final float DEFAULT_LOAD_FACTOR = 0.75f; // Entry数组,长度必须为2的幂 Entry<K,V>[] table; // 元素个数 private int size; // 阈值 private int threshold; // 装载因子 private final float loadFactor; // 引用队列 private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 修改次数 int modCount; 跟HashMap的成员变量几乎一致,这里多了一个ReferenceQueue,用来存放那些已经被回收了的弱引用对象。如果想知道ReferenceQueue是如何工作的,可以参考这篇文章。 构造函数 WeakHashMap中也有四个构造函数: public WeakHashMap(int initialCapacity, float loadFactor) { ... } public WeakHashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public WeakHashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public WeakHashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAll(m); } 可以看到后三个,都是调用的第一个构造函数,下面再来看一下第一个构造函数的内容: // 校验initialCapacity if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Initial Capacity: "+ initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 校验loadFactor if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load factor: "+ loadFactor); int capacity = 1; // 将容量设置为大于initialCapacity的最小2的幂 while (capacity < initialCapacity) capacity <<= 1; table = newTable(capacity); this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); 再看看newTable函数。 private Entry<K,V>[] newTable(int n) { return (Entry<K,V>[]) new Entry<?,?>[n]; } 这里其实只是简单的创建一个Entry数组。 Entry剖析 接下来看看WeakHashMap中的核心角色——Entry。上面已经看到了,WeakHashMap中的table是一个Entry数组: Entry<K,V>[] table; 来看看Entry长什么样: private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { ... } Entry继承自WeakReference,继承关系图如下: 再来看看Entry中的内容: // 成员变量 V value; final int hash; Entry<K,V> next; // 构造函数 Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) { super(key, queue); this.value = value; this.hash = hash; this.next = next; } 细心的你可能会发现,哎?key哪里去了,成员变量里没有key。别着急,看看构造函数就可以发现,它调用了父类的构造函数。 super(key, queue); 这里调用的WeakReference的构造函数,将key传入Reference中,保存在referent成员变量中。对Reference和WeakReference不熟悉的话可以参考这篇文章和这篇文章。 再看看其它几个方法: @SuppressWarnings("unchecked") public K getKey() { // 这里调用了Reference的get方法,从中取出referent对象 // WeakHashMap中,key如果为null会使用NULL_KEY来替代 return (K) WeakHashMap.unmaskNull(get()); } public V getValue() { return value; } public V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; K k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { V v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public int hashCode() { K k = getKey(); V v = getValue(); // 这里只是简单的把key和value的hashcode做一个异或处理 return Objects.hashCode(k) ^ Objects.hashCode(v); } public String toString() { return getKey() + "=" + getValue(); } 这里稍微说一下getKey方法,调用了WeakHashMap.unmaskNull,之所以要调用这个方法,其实是因为WeakHashMap中对key为null时的特殊处理,会将其指向一个特殊的内部变量: private static final Object NULL_KEY = new Object(); 与其对应的两个方法便是: private static Object maskNull(Object key) { return (key == null) ? NULL_KEY : key; } static Object unmaskNull(Object key) { return (key == NULL_KEY) ? null : key; } 所以,其他WeakHashMap中的Entry最大的不同就是继承自WeakReference,并把key保存在了WeakReference中。可以说WeakHashMap的特性绝大部分都是WeakReference的功劳。 常用方法 主要的方法有这些: void clear() Object clone() boolean containsKey(Object key) boolean containsValue(Object value) Set<Entry<K, V>> entrySet() V get(Object key) boolean isEmpty() Set<K> keySet() V put(K key, V value) void putAll(Map<? extends K, ? extends V> map) V remove(Object key) int size() Collection<V> values() 这里选其中的三个最常用的方法进行解析: put方法 public V put(K key, V value) { // 处理null值 Object k = maskNull(key); // 计算hash int h = hash(k); // 获取table Entry<K,V>[] tab = getTable(); // 计算下标 int i = indexFor(h, tab.length); // 查找Entry for (Entry<K,V> e = tab[i]; e != null; e = e.next) { if (h == e.hash && eq(k, e.get())) { V oldValue = e.value; if (value != oldValue) e.value = value; return oldValue; } } modCount++; Entry<K,V> e = tab[i]; tab[i] = new Entry<>(k, value, queue, h, e); // 如果元素个数超过阈值,则进行扩容 if (++size >= threshold) resize(tab.length * 2); return null; } 这里涉及到的方法比较多,不慌不慌,一个一个来。 先来看看hash方法: final int hash(Object k) { int h = k.hashCode(); // 这里做了二次散列,来扩大低位的影响 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } hash方法对key的hashcode进行了二次散列,主要是为了扩大低位的影响。因为Entry数组的大小是2的幂,在进行查找的时候,进行掩码处理,如果不进行二次散列,那么低位对index就完全没有影响了,如果不清楚也没有关系,之后在get方法里会有说明。 至于为什么要选20,12,7,4。emmm,大概是效果奇佳吧(一本正经的胡说八道,有兴趣的话可以自行研究)。 再看看indexFor函数,这里就是将数组长度减1后与hashcode做一个位与操作,因为length必定是2的幂,所以减1后就变成了掩码,再进行与操作就能直接得到hashcode mod length的结果了,但是这样操作效率会更高。 private static int indexFor(int h, int length) { return h & (length-1); } 再来看看getTable方法: private Entry<K,V>[] getTable() { // 清除被回收的Entry对象 expungeStaleEntries(); return table; } private void expungeStaleEntries() { for (Object x; (x = queue.poll()) != null; ) { // 循环获取引用队列中的对象 synchronized (queue) { @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) x; // 查找对应的位置 int i = indexFor(e.hash, table.length); // 找到之前的Entry Entry<K,V> prev = table[i]; Entry<K,V> p = prev; // 在链表中寻找 while (p != null) { Entry<K,V> next = p.next; if (p == e) { if (prev == e) table[i] = next; else prev.next = next; // 将对应的value置为null,帮助GC回收 e.value = null; size--; break; } prev = p; p = next; } } } } 所以每次调用getTable的时候,都会将table中key已经被回收掉的Entry移除掉。 resize方法: void resize(int newCapacity) { // 获取当前table Entry<K,V>[] oldTable = getTable(); int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 新建一个table Entry<K,V>[] newTable = newTable(newCapacity); // 将旧table中的内容复制到新table中 transfer(oldTable, newTable); table = newTable; if (size >= threshold / 2) { threshold = (int)(newCapacity * loadFactor); } else { expungeStaleEntries(); transfer(newTable, oldTable); table = oldTable; } } // 新建Entry数组 private Entry<K,V>[] newTable(int n) { return (Entry<K,V>[]) new Entry<?,?>[n]; } private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) { for (int j = 0; j < src.length; ++j) { Entry<K,V> e = src[j]; src[j] = null; while (e != null) { Entry<K,V> next = e.next; Object key = e.get(); if (key == null) { e.next = null; e.value = null; size--; } else { int i = indexFor(e.hash, dest.length); e.next = dest[i]; dest[i] = e; } e = next; } } } get方法 public V get(Object key) { // 对null值特殊处理 Object k = maskNull(key); // 取key的hash值 int h = hash(k); // 取当前table Entry<K,V>[] tab = getTable(); // 获取下标 int index = indexFor(h, tab.length); Entry<K,V> e = tab[index]; // 链表中查找元素 while (e != null) { if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } return null; } 在查找元素的时候调用了一个eq方法: private static boolean eq(Object x, Object y) { return x == y || x.equals(y); } remove方法 public V remove(Object key) { // 对null值特殊处理 Object k = maskNull(key); // 取key的hash int h = hash(k); // 取当前table Entry<K,V>[] tab = getTable(); // 计算下标 int i = indexFor(h, tab.length); Entry<K,V> prev = tab[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; // 查找对应Entry if (h == e.hash && eq(k, e.get())) { modCount++; size--; if (prev == e) tab[i] = next; else prev.next = next; // 如果找到,返回对应Entry的value return e.value; } prev = e; e = next; } return null; } 使用栗子 public class WeakHashMapTest { public static void main(String[] args){ testWeakHashMap(); } private static void testWeakHashMap() { // 创建3个String对象用来做key String w1 = new String("key1"); String w2 = new String("key2"); String w3 = new String("key3"); // 新建WeakHashMap Map weakHashMap = new WeakHashMap(); // 添加键值对 weakHashMap.put(w1, "v1"); weakHashMap.put(w2, "v2"); weakHashMap.put(w3, "v3"); // 打印出weakHashMap System.out.printf("weakHashMap:%s\n", weakHashMap); // containsKey(Object key) :是否包含键key System.out.printf("contains key key1 : %s\n",weakHashMap.containsKey("key1")); System.out.printf("contains key key4 : %s\n",weakHashMap.containsKey("key4")); // containsValue(Object value) :是否包含值value System.out.printf("contains value v1 : %s\n",weakHashMap.containsValue("v1")); System.out.printf("contains value 0 : %s\n",weakHashMap.containsValue(0)); // remove(Object key) : 删除键key对应的键值对 weakHashMap.remove("three"); System.out.printf("weakHashMap: %s\n", weakHashMap); // ---- 测试 WeakHashMap 的自动回收特性 ---- // 将w1设置null。 // 这意味着“弱键”w1再没有被其它对象引用,调用gc时会回收WeakHashMap中与“w1”对应的键值对 w1 = null; // 内存回收。这里,会回收WeakHashMap中与“w1”对应的键值对 System.gc(); // 遍历WeakHashMap Iterator iter = weakHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry en = (Map.Entry)iter.next(); System.out.printf("next : %s - %s\n",en.getKey(),en.getValue()); } // 打印WeakHashMap的实际大小 System.out.printf("after gc WeakHashMap size:%s\n", weakHashMap.size()); } } 输出如下: weakHashMap:{key1=w1, key2=w2, key3=w3} contains key key1 : true contains key key4 : false contains value w1 : true contains value 0 : false weakHashMap: {key1=w1, key2=w2, key3=w3} next : key2 - w2 next : key3 - w3 after gc WeakHashMap size:2 可以看到,w1对应的Entry被回收掉了,这就是WeakHashMap的最重要特性,当然,实际使用的时候一般不会这样使用, 应用场景 由于WeakHashMap可以自动清除Entry,所以比较适合用于存储非必需对象,用作缓存非常合适。 public final class ConcurrentCache<K,V> { private final int size; private final Map<K,V> eden; private final Map<K,V> longterm; public ConcurrentCache(int size) { this.size = size; this.eden = new ConcurrentHashMap<>(size); this.longterm = new WeakHashMap<>(size); } public V get(K k) { V v = this.eden.get(k); if (v == null) { synchronized (longterm) { v = this.longterm.get(k); } if (v != null) { this.eden.put(k, v); } } return v; } public void put(K k, V v) { if (this.eden.size() >= size) { synchronized (longterm) { this.longterm.putAll(this.eden); } this.eden.clear(); } this.eden.put(k, v); } } 在put方法里,在插入一个键值对时,先检查eden缓存的容量是不是超过了阈值,如果没有超就直接放入eden缓存,如果超了就将eden中所有的键值对都放入longterm(这里longterm类似于老年代,eden类似于年轻代),再将eden清空并插入相应键值对。 在get方法中,也是优先从eden中找对应的value,如果没有则进入longterm缓存中查找,找到后就加入eden缓存并返回。 这样设计的好处是,能将相对常用的对象都能在eden缓存中找到,不常用的则存入longterm缓存,并且由于WeakHashMap能自动清除Entry,所以不用担心longterm中键值对过多而导致OOM。 WeakHashMap还有这样一个不错的应用场景,配合事务进行使用,存储事务过程中的各类信息。可以使用如下结构: WeakHashMap<String,Map<K,V>> transactionCache; 这里key为String类型,可以用来标志区分不同的事务,起到一个事务id的作用。value是一个map,可以是一个简单的HashMap或者LinkedHashMap,用来存放在事务中需要使用到的信息。 在事务开始时创建一个事务id,并用它来作为key,事务结束后,将这个强引用消除掉,这样既能保证在事务中可以获取到所需要的信息,又能自动释放掉map中的所有信息。 小结 WeakHashMap是一个会自动清除Entry的Map WeakHashMap的操作与HashMap完全一致 WeakHashMap内部数据结构是数组+链表 WeakHashMap常被用作缓存

优秀的个人博客,低调大师

深度学习入门笔记系列 ( 八 ) ——基于 tensorflow 的手写数字的识别(进阶)

基于 tensorflow 的手写数字的识别(进阶) 本系列将分为 8 篇 。本次为第 8 篇 ,基于 tensorflow ,利用卷积神经网络 CNN 进行手写数字识别 。 1.引言 关于 mnist 数据集的介绍和卷积神经网络的笔记在本系列文章中已有过介绍 ,有需要可见下述两篇文章 。本系列第 5 篇曾实现利用最简单的 BP 神经网络进行手写数字识别 。本系列第 6 篇简单介绍了下卷积神经网络的知识 。 基于 tensorflow 的手写数字识别 卷积神经网络(CNN)学习笔记 2.设计的 CNN 结构 本系列第 4 讲讲过实战可以大致分为 "三步走" ● 定义神经网络的结构和前向传播的输出结果 ● 定义损失函数以及选择反向传播优化的算法 ● 生成会话(tf.Session) 并在训练数据上反复运行反向传播优化算法 这里也一样 ,当然首先是设计我们针对此实战的卷积神经网络 ,设计一个最简单的如下手绘 (还是那句话 ,字丑人帅 ,拒绝反驳) 上图得到两次卷积池化结果后 ,将结果展平为 1 维向量 ,即1 *(7*7*64),再连接到十个节点的输出层 。 3.手动干起来 ! 首先 ,需要读取 MNIST 数据集 ,利用 TF 框架自带类进行下载读取 。 接下来就是根据之前的 “三步走” 进行实践 。实现上述的网络结构 ,并依旧选择二次代价函数和梯度下降法 。 首先 ,定义两个函数 ,用于初始化参数 。再定义两个函数实现卷积核池化(只是便于模块化 ,提高可读性)。 根据上述手绘结构图进行编程实现该结构 。 这里有一个 dropout 操作 ,目的是训练过程中使一部分神经元参数不变 ,即不参与训练 ,相当于简化结构 ,减少过拟合 。 再在会话 Session 中执行 ,并保存好模型参数 。 测试结果(小詹在按时付费的某服务器跑的结果)如下图 : 原文发布时间为:2018-09-14 本文作者:小詹本文来自云栖社区合作伙伴“ 小詹学Python”,了解相关信息可以关注“ 小詹学Python”。

优秀的个人博客,低调大师

【Java入门提高篇】Day33 Java容器类详解(十五)PriorityQueue详解

今天要介绍的是基础容器类(为了与并发容器类区分开来而命名的名字)中的另一个成员——PriorityQueue,它的大名叫做优先级队列,想必即使没有用过也该有所耳闻吧,什么?没。。没听过?emmm。。。那就更该认真看看了。 通过本篇你将了解到: 1、PriorityQueue是什么? 2、PriorityQueue的内部结构是什么? 3、二叉堆、大顶堆、小顶堆分别是什么?有什么特性? 4、小顶堆是如何实现的,如何用数组表示? 5、小顶堆的删除、插入操作是如何进行的? 6、PriorityQueue的源码解析。 7、PriorityQueue的应用场景。 一、PriorityQueue简介 PriorityQueue也是Queue的一个继承者,相比于一般的列表,它的特点便如它的名字一样,出队的时候可以按照优先级进行出队,所以不像LinkedList那样只能按照插入的顺序出队,PriorityQueue是可以根据给定的优先级顺序进行出队的。这里说的给定优先级顺序既可以是内部比较器,也可以是外部比较器。PriorityQueue内部是根据小顶堆的结构进行存储的,所谓小顶堆的意思,便是最小的元素总是在最上面,每次出队总是将堆顶元素移除,这样便能让出队变得有序,至于什么是小顶堆,后面会有详细介绍。 比如说,比较常见的场景就是任务队列,队列动态插入,后面的任务优先级高的需要被先执行,那么使用优先级队列就可以比较好的实现这样的需求。下面我们模拟一下这个场景: public class PriorityQueueTest { public static void main(String[] args){ // 传入外部比较器, //PriorityQueue<Task> taskQueue = new PriorityQueue<>(Comparator.comparingInt(Task::getPriority)); //PriorityQueue<Task> taskQueue = new PriorityQueue<>((t1, t2) -> t1.getPriority() - t2.getPriority()); PriorityQueue<Task> taskQueue = new PriorityQueue<>(new Comparator<Task>() { @Override public int compare(Task t1, Task t2) { return t1.getPriority() - t2.getPriority(); } }); // 添加六个任务 taskQueue.add(new Task(1, "learn java")); taskQueue.add(new Task(3, "learn c++")); taskQueue.add(new Task(4, "learn c#")); taskQueue.add(new Task(2, "learn python")); taskQueue.add(new Task(2, "learn php")); taskQueue.add(new Task(5, "learn js")); // 出队 while (!taskQueue.isEmpty()){ System.out.println(taskQueue.poll()); } } } class Task{ /** * 任务优先级 */ private int priority; /** * 任务名称 */ private String taskName; public Task() { } public Task(int priority, String taskName) { this.priority = priority; this.taskName = taskName; } public int getPriority() { return priority; } public void setPriority(int priority) { this.priority = priority; } public String getTaskName() { return taskName; } public void setTaskName(String taskName) { this.taskName = taskName; } @Override public String toString() { return "Task{" + "priority=" + priority + ", taskName='" + taskName + '\'' + '}'; } } 输出如下: Task{priority=1, taskName='learn java'} Task{priority=2, taskName='learn python'} Task{priority=2, taskName='learn php'} Task{priority=3, taskName='learn c++'} Task{priority=4, taskName='learn c#'} Task{priority=5, taskName='learn js'} 可以看到,输出的时候是按照我们设定的优先级顺序进行输出的,由于默认的是小顶堆,所以这里Priority值小的会被先输出。 二、PriorityQueue的内部结构 上面已经提到了,PriorityQueue的内部结构其实是按照小顶堆的结构进行存储的,那么什么是小顶堆呢?说到小顶堆,还是先从堆开始介绍吧。 堆和栈一样是一种很基础的数据结构,在维基百科中的介绍如下: 堆(英语:Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。 用图来表示的话就像这样: 说完了堆,再来聊聊它的进化版——二叉堆,同样引用维基百科中的介绍: 二叉堆(英语:binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。 当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。 其中,最大堆也叫做大顶堆或者大根堆,最小堆也叫做小顶堆或者小根堆。上面的图一其实就是一个大顶堆,而图二则是小顶堆。PriorityQueue是通过数组表示的小顶堆实现的,既然如此,PriorityQueue的排序特性自然与小顶堆的特性一致,下面便介绍小顶堆如何使用数组进行表示以及插入删除时的调整。 下面是一个由10,16,20,22,18,25,26,30,24,23构成的小顶堆: 将其从第一个元素开始依次从上到下,从左到右给每个元素添加一个序号,从0开始,这样就得到了相应元素在数组中的位置,而且这个序号是很有规则的,第k个元素的左孩子的序号为2k+1,右孩子的序号为2k+2,这样就很容易根据序号直接算出对应孩子的位置,时间复杂度为o(1)。这也就是为什么可以用数组来存储堆结构的原因了。 再来看看小顶堆是如何插入元素的,假设我们插入一个元素15: 插入元素的调整其实很简单,就是先插入到最后,然后再依次与其父节点进行比较,如果小于其父节点,则互换,直到不需要调整或者父节点为null为止。 那再来看看移除元素: 嗯,过程其实也很简单,先用最后的元素当替补,然后再从上往下进行调整。 三、PriorityQueue源码解析 小顶堆已经介绍完了,那PriorityQueue就没什么内容可讲了,嗯,那散了吧好了好了,不开玩笑了,接下来让我们一起来看看源码中是如何实现的。 先来继承结构: PriorityQueue继承自AbstractQueue,像这样的Abstract开头的抽象类,想必应该不陌生了,就是继承自指定接口然后进行了一些默认实现。 来看看PriorityQueue的内部成员: // 默认初始化容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; /** * 优先级队列是使用平衡二叉堆表示的: 节点queue[n]的两个孩子分别为 * queue[2*n+1] 和 queue[2*(n+1)]. 队列的优先级是由比较器或者 * 元素的自然排序决定的, 对于堆中的任意元素n,其后代d满足:n<=d * 如果堆是非空的,则堆中最小值为queue[0]。 */ transient Object[] queue; /** * 队列中元素个数 */ private int size = 0; /** * 比较器 */ private final Comparator<? super E> comparator; /** * 修改次数 */ transient int modCount = 0; 可以看到内部使用的是一个Object数组进行元素的存储,并对该数组进行了详细的注释,所以不管是根据子节点找父节点,还是根据父节点找子节点都肥肠的方便。 再来看看它的构造函数,有点多,一共有六个构造函数: /** * 使用默认的容量(11)来构造一个空的优先级队列,使用元素的自然顺序进行排序(此时元素必须实现comparable接口) */ public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } /** * 使用指定容量来构造一个空的优先级队列,使用元素的自然顺序进行排序(此时元素必须实现comparable接口) * 但如果指定的容量小于1则会抛出异常 */ public PriorityQueue(int initialCapacity) { this(initialCapacity, null); } /** * 使用默认的容量(11)构造一个优先级队列,使用指定的比较器进行排序 */ public PriorityQueue(Comparator<? super E> comparator) { this(DEFAULT_INITIAL_CAPACITY, comparator); } /** * 使用指定容量创建一个优先级队列,并使用指定比较器进行排序。 * 但如果指定的容量小于1则会抛出异常 */ public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; } /** * 使用指定集合的所有元素构造一个优先级队列, * 如果该集合为SortedSet或者PriorityQueue类型,则会使用相同的顺序进行排序, * 否则,将使用元素的自然排序(此时元素必须实现comparable接口),否则会抛出异常 * 并且集合中不能有null元素,否则会抛出异常 */ @SuppressWarnings("unchecked") public PriorityQueue(Collection<? extends E> c) { if (c instanceof SortedSet<?>) { SortedSet<? extends E> ss = (SortedSet<? extends E>) c; this.comparator = (Comparator<? super E>) ss.comparator(); initElementsFromCollection(ss); } else if (c instanceof PriorityQueue<?>) { PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c; this.comparator = (Comparator<? super E>) pq.comparator(); initFromPriorityQueue(pq); } else { this.comparator = null; initFromCollection(c); } } /** * 使用指定的优先级队列中所有元素来构造一个新的优先级队列. 将使用原有顺序进行排序。 */ @SuppressWarnings("unchecked") public PriorityQueue(PriorityQueue<? extends E> c) { this.comparator = (Comparator<? super E>) c.comparator(); initFromPriorityQueue(c); } /** * 根据指定的有序集合创建一个优先级队列,将使用原有顺序进行排序 */ @SuppressWarnings("unchecked") public PriorityQueue(SortedSet<? extends E> c) { this.comparator = (Comparator<? super E>) c.comparator(); initElementsFromCollection(c); } 从集合中构造优先级队列的时候,调用了几个初始化函数: private void initFromPriorityQueue(PriorityQueue<? extends E> c) { if (c.getClass() == PriorityQueue.class) { this.queue = c.toArray(); this.size = c.size(); } else { initFromCollection(c); } } private void initElementsFromCollection(Collection<? extends E> c) { Object[] a = c.toArray(); // If c.toArray incorrectly doesn't return Object[], copy it. if (a.getClass() != Object[].class) a = Arrays.copyOf(a, a.length, Object[].class); int len = a.length; if (len == 1 || this.comparator != null) for (int i = 0; i < len; i++) if (a[i] == null) throw new NullPointerException(); this.queue = a; this.size = a.length; } private void initFromCollection(Collection<? extends E> c) { initElementsFromCollection(c); heapify(); } initFromPriorityQueue即从另外一个优先级队列构造一个新的优先级队列,此时内部的数组元素不需要进行调整,只需要将原数组元素都复制过来即可。但是从其他非PriorityQueue的集合中构造优先级队列时,需要先将元素复制过来后再进行调整,此时调用的是heapify方法: private void heapify() { // 从最后一个非叶子节点开始从下往上调整 for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); } // 划重点了,这个函数即对应上面的元素删除时从上往下调整的步骤 private void siftDown(int k, E x) { if (comparator != null) // 如果比较器不为null,则使用比较器进行比较 siftDownUsingComparator(k, x); else // 否则使用元素的compareTo方法进行比较 siftDownComparable(k, x); } private void siftDownUsingComparator(int k, E x) { // 使用half记录队列size的一半,如果比half小的话,说明不是叶子节点 // 因为最后一个节点的序号为size - 1,其父节点的序号为(size - 2) / 2或者(size - 3 ) / 2 // 所以half所在位置刚好是第一个叶子节点 int half = size >>> 1; while (k < half) { // 如果不是叶子节点,找出其孩子中较小的那个并用其替换 int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; // 用c替换 queue[k] = c; k = child; } // queue[k] = x; } // 同上,只是比较的时候使用的是元素的compareTo方法 private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // 如果是非叶子节点则继续循环 while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = key; } 这里可能一眼看过去有点难以理解,嗯,那就多看两眼吧。 siftDown方法是这里面比较重要的方法之一,有两个参数,一个是序号k,另一个是元素x,这个方法的作用,便是把x从k开始往下调整,使得在节点k在其子树的每相邻层中,父节点都小于其子节点。所以heapify的作用就比较明显了,从最后一个非叶子节点开始,从下往上依次调整其子树,使得最终得到的树里,根节点是最小的。这里要先理解一下为什么heapify中i的初始值要设置为(size >>> 1) - 1。因为这是最后一个非叶子节点的位置,不信的话可以随便画几个图验证一下,至于在siftDownUsingComparator方法中,int half = size >>> 1;这里half则是第一个叶子节点的位置,小于这个序号的节点都是非叶子节点,这里也可以画图验证,当然,注释中我已经做了解释。 说了这么多,也许还是不太明白,以集合{14,7,12,6,9,4,17,23,10,15,3}为例画个图吧: 嗯,这样最小的元素就被顶上去了,有没有觉得有点像冒泡排序,嗯,确实有点像。 siftDown说完了,再来看一眼siftUp吧,这里操作是十分类似的。 private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); } @SuppressWarnings("unchecked") private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = key; } @SuppressWarnings("unchecked") private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; } 嗯,相信如果理解了siftDown的话,这里应该就不难理解了吧,如果还有疑问,欢迎留言。 再来看看几个常用的方法: public boolean add(E e) { return offer(e); } public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else siftUp(i, e); return true; } // 扩容函数 private void grow(int minCapacity) { int oldCapacity = queue.length; // 如果当前容量比较小(小于64)的话进行双倍扩容,否则扩容50% int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // 如果发现扩容后溢出了,则进行调整 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } public boolean contains(Object o) { return indexOf(o) != -1; } private int indexOf(Object o) { if (o != null) { // 查找时需要进行全局遍历,比搜索二叉树的查找效率要低 for (int i = 0; i < size; i++) if (o.equals(queue[i])) return i; } return -1; } public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result; } 这里对照一下最开始说的小顶堆的插入和移除就能比较好的理解了。 最后源码中还有一个remove方法,需要稍微说明一下: // 这里不是移除堆顶元素,而是移除指定元素 public boolean remove(Object o) { // 先找到该元素的位置 int i = indexOf(o); if (i == -1) return false; else { removeAt(i); return true; } } // 移除指定序号的元素 private E removeAt(int i) { // assert i >= 0 && i < size; modCount++; // s为最后一个元素的序号 int s = --size; if (s == i) queue[i] = null; else { // moved记录最后一个元素的值 E moved = (E) queue[s]; queue[s] = null; // 用最后一个元素代替要移除的元素,并向下进行调整 siftDown(i, moved); // 如果向下调整后发现moved还在该位置,则再向上进行调整 if (queue[i] == moved) { siftUp(i, moved); if (queue[i] != moved) return moved; } } return null; } 当移除的不是堆顶元素的时候,同样先用最后一个元素代替,然后先从被移除的位置开始向下调整,如果发现没有改动,则再向上调整。 四、PriorityQueue的应用场景 最后,来聊聊PriorityQueue的应用场景,由于内部是用数组实现的小顶堆,所以堆适用的场景它都适用,比如典型的从n个元素中取出最小(最大)的前k个,这样的场景适用PriorityQueue就能以比较小的空间代价和还算ok的时间代价进行实现,另外,优先级队列适用场景的特点便是需要动态插入元素,并且元素有优先级,需要根据一定的规则进行优先级排序。 下面以从10000个整数中取出最大的10个整数为例进行介绍。 public class Test { public static void main(String[] args){ ArrayList<Integer> integers = new ArrayList<>(10000); Random random = new Random(); for (int i = 0; i < 10000; i++) { Integer integer = random.nextInt(); if (!integers.contains(integer)) integers.add(integer); } Integer[] largest = getLargest10(integers); for (Integer i : largest){ System.out.print(i + " "); } System.out.println(); // 验证一下是否是最大的前10个 integers.sort(Comparator.comparingInt(Integer::intValue)); ArrayList<Integer> largest2 = new ArrayList<>(10); for (int i = integers.size() - 1; i >= integers.size() - 10; i--){ largest2.add(integers.get(i)); } // 在largest数组中查找 System.out.println(Arrays.asList(largest).containsAll(largest2)); } public static Integer[] getLargest10(ArrayList<Integer> integers){ PriorityQueue<Integer> queue = new PriorityQueue<>(10); for (Integer integer : integers){ queue.add(integer); if (queue.size() > 10){ queue.poll(); } } return queue.toArray(new Integer[10]); } } 输出如下,由于是取的随机数,所以每个人的输出都会不一样。 2143974860 2143998490 2144350843 2145111627 2144739333 2145674658 2144667271 2145543903 2147209906 2145466260 true 最后,我们来回答一下开头的问题: 1、PriorityQueue是什么?PriorityQueue是优先级队列,取出元素时会根据元素的优先级进行排序。 2、PriorityQueue的内部结构是什么?PriorityQueue内部是一个用数组实现的小顶堆。 3、二叉堆、大顶堆、小顶堆分别是什么?有什么特性?二叉堆是完全二叉树或者近完全二叉树,大顶堆即所有父节点大于子节点,小顶堆即所有父节点小于子节点。 4、小顶堆是如何实现的,如何用数组表示?小顶堆是用二叉树实现的,用数组表示时,父节点n的左孩子为2n+1,右孩子的序号为2n+2。 5、小顶堆的删除、插入操作是如何进行的?小顶堆删除堆顶元素后用最后一个元素替补,然后从上往下调整,插入一个元素时,先放到最后的位置,然后再从下往上调整。 6、PriorityQueue的源码解析。如上。 7、PriorityQueue的应用场景。适用于需要动态插入元素,且元素有优先级顺序的场景。 到此,本篇圆满结束。如果觉得还不错的话,记得动动小手点个赞,也欢迎关注博主,你们的支持是我写出更好博客的动力。 有兴趣对Java进行更深入学习和交流的小伙伴,欢迎加入QQ群交流:529253292

优秀的个人博客,低调大师

【Java入门提高篇】Day32 Java容器类详解(十四)ArrayDeque详解

今天来介绍一个不太常见也不太常用的类——ArrayDeque,这是一个很不错的容器类,如果对它还不了解的话,那么就好好看看这篇文章吧。 看完本篇,你将会了解到: 1、ArrayDeque是什么? 2、ArrayDeque如何使用? 3、ArrayDeque的内部结构是怎样的? 4、ArrayDeque的各个方法是如何实现的? 5、ArrayDeque是如何扩容的? 6、ArrayDeque的容量有什么限制? 7、ArrayDeque和LinkedList相比有什么优势? 8、ArrayDeque的应用场景是什么? 一、ArrayDeque简介 ArrayDeque是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。听到这里,不熟悉ArrayDeque的你是不是有点尴尬?JDK中竟然还有这么好的一个容器类? 别慌,现在了解还来得及,趁响指还没有弹下去,快上车吧,没时间解释了。 来看一个ArrayDeque的使用小栗子: public class DequeTest { public static void main(String[] args){ // 初始化容量为4 ArrayDeque<String> arrayDeque = new ArrayDeque<>(4); //添加元素 arrayDeque.add("A"); arrayDeque.add("B"); arrayDeque.add("C"); arrayDeque.add("D"); arrayDeque.add("E"); arrayDeque.add("F"); arrayDeque.add("G"); arrayDeque.add("H"); arrayDeque.add("I"); System.out.println(arrayDeque); // 获取元素 String a = arrayDeque.getFirst(); String a1 = arrayDeque.pop(); String b = arrayDeque.element(); String b1 = arrayDeque.removeFirst(); String c = arrayDeque.peek(); String c1 = arrayDeque.poll(); String d = arrayDeque.pollFirst(); String i = arrayDeque.pollLast(); String e = arrayDeque.peekFirst(); String h = arrayDeque.peekLast(); String h1 = arrayDeque.removeLast(); System.out.printf("a = %s, a1 = %s, b = %s, b1 = %s, c = %s, c1 = %s, d = %s, i = %s, e = %s, h = %s, h1 = %s", a,a1,b,b1,c,c1,d,i,e,h,h1); System.out.println(); // 添加元素 arrayDeque.push(e); arrayDeque.add(h); arrayDeque.offer(d); arrayDeque.offerFirst(i); arrayDeque.offerLast(c); arrayDeque.offerLast(h); arrayDeque.offerLast(c); arrayDeque.offerLast(h); arrayDeque.offerLast(i); arrayDeque.offerLast(c); System.out.println(arrayDeque); // 移除第一次出现的C arrayDeque.removeFirstOccurrence(c); System.out.println(arrayDeque); // 移除最后一次出现的C arrayDeque.removeLastOccurrence(c); System.out.println(arrayDeque); } } 输出如下: [A, B, C, D, E, F, G, H, I] a = A, a1 = A, b = B, b1 = B, c = C, c1 = C, d = D, i = I, e = E, h = H, h1 = H [I, E, E, F, G, H, D, C, H, C, H, I, C] [I, E, E, F, G, H, D, H, C, H, I, C] [I, E, E, F, G, H, D, H, C, H, I] 可以看到,从ArrayDeque中取出元素的姿势可谓是五花八门,不过别慌,稍后会对这些方法进行一一讲解,现在只需要知道,get、peek、element方法都是获取元素,但是不会将它移除,而pop、poll、remove都会将元素移除并返回,add和push、offer都是插入元素,它们的不同点在于插入元素的位置以及插入失败后的结果。 二、ArrayDeque的内部结构 ArrayDeque的整体继承结构如下: ArrayDeque是继承自Deque接口,Deque继承自Queue接口,Queue是队列,而Deque是双端队列,也就是可以从前或者从后插入或者取出元素,也就是比队列存取更加方便一点,单向队列只能从一头插入,从另一头取出。 再来看看ArrayDeque的内部结构,其实从名字就可以看出来,ArrayDeque自然是基于Array的双端队列,内部结构自然是数组: //存储元素的数组 transient Object[] elements; // 非private访问限制,以便内部类访问 /** * 头部节点序号 */ transient int head; /** * 尾部节点序号,(指向最后一点节点的后一个位置) */ transient int tail; /** * 双端队列的最小容量,必须是2的幂 */ private static final int MIN_INITIAL_CAPACITY = 8; 这里可以看到,元素都存储在Object数组中,head记录首节点的序号,tail记录尾节点后一个位置的序号,队列的容量最小为8,而且必须为2的幂。看到这里,有没有想到HashMap的元素个数限制也必须为2的幂,嗯,这里同HashMap一样,自有妙用,后面会有分析。 三、ArrayDeque的常用方法 从队列首部插入/取出 从队列尾部插入/取出 失败抛出异常 失败返回特殊值 失败抛出异常 失败返回特殊值 插入 addFirst(e) push() offerFirst(e) addLast(e) offerLast(e) 移除 removeFirst() pop() pollFirst() removeLast() pollLast() 获取 getFirst() peekFirst() getLast() peekLast() 嗯,几乎绝大多数常用方法都在这里了,基本上可以分成两类,一类是以add,remove,get开头的方法,这类方法失败后会抛出异常,一类是以offer,poll,peek开头的方法,这类方法失败之后会返回特殊值,如null。大部分方法基本上都是可以根据命名来推断其作用,如addFirst,当然就是从队列头部插入,removeLast,便是从队列尾部移除,get和peek只获取元素而不移除,getFirst方法调用时,如果队列为空,会抛出NoSuchElementException异常,而peekFirst在队列为空时调用则返回null。 一下摆出这么多方法有些难以接受?别慌别慌,接下来让我们从源码的角度一起来看看这些方法,用图说话,来解释我们最开始那个栗子中到底发生了哪些事情。 四、ArrayDeque源码分析 先来看看构造函数: /** * 构造一个初始容量为16的空队列 */ public ArrayDeque() { elements = new Object[16]; } /** * 构造一个能容纳指定大小的空队列 */ public ArrayDeque(int numElements) { allocateElements(numElements); } /** * 构造一个包含指定集合所有元素的队列 */ public ArrayDeque(Collection<? extends E> c) { allocateElements(c.size()); addAll(c); } 所以之前栗子中, ArrayDeque<String> arrayDeque = new ArrayDeque<>(4); 调用的是第二个构造函数,里面有这么一个函数allocateElements,让我们来看看它的实现: 1 private void allocateElements(int numElements) { 2 elements = new Object[calculateSize(numElements)]; 3 } 4 5 private static int calculateSize(int numElements) { 6 int initialCapacity = MIN_INITIAL_CAPACITY; 7 if (numElements >= initialCapacity) { 8 initialCapacity = numElements; 9 initialCapacity |= (initialCapacity >>> 1); 10 initialCapacity |= (initialCapacity >>> 2); 11 initialCapacity |= (initialCapacity >>> 4); 12 initialCapacity |= (initialCapacity >>> 8); 13 initialCapacity |= (initialCapacity >>> 16); 14 initialCapacity++; 15 16 if (initialCapacity < 0) 17 initialCapacity >>>= 1; 18 } 19 return initialCapacity; 20 } allocateElements方法主要用于给内部的数组分配合适大小的空间,calculateSize方法用于计算比numElements大的最小2的幂次方,如果指定的容量大小小于MIN_INITIAL_CAPACITY(值为8),那么将容量设置为8,否则通过多次无符号右移进行最小2次幂计算。先将initialCapacity赋值为numElements,接下来,进行5次无符号右移,下面将以一个小栗子介绍这样运算的妙处。 在Java中,int类型是占4字节,也就是32位。简单起见,这里以一个8位二进制数来演示前三次操作。假设这个二进制数对应的十进制为89,整个过程如下: 可以看到最后,除了第一位,其他位全部变成了1,然后这个结果再加一,即得到大于89的最小的2次幂,怎么样,很巧妙吧,也许你会想,为什么右移的数值要分别是1,2,4,8,16呢?嗯,好问题。其实仔细观察就会发现,先右移在进行或操作,其实我们只需要关注第一个不为0的位即可,下面以64为例再演示一次: 所以,事实上,在这系列操作中,其他位只是配角,我们只需要关注第一个不为0的位即可,假设其为第n位,先右移一位然后进行或操作,得到的结果,第n位和第n-1位肯定为1,这样就有两个位为1了,然后进行右移两位,再进行或操作,那么第n位到第n-3位一定都为1,然后右移4位,依次类推。int为32位,因此,最后只需要移动16位即可。1+2+4+8+16 = 31,所以经过这一波操作,原数字对应的二进制,操作得到的结果将是从其第一个不为0的位开始,往后的位均为1。然后: initialCapacity++; 再自增一下,目标完成。观察到还有下面这一小节代码: if (initialCapacity < 0) initialCapacity >>>= 1; 其实它是为了防止进行这一波操作之后,得到了负数,即原来第31位为1,得到的结果第32位将为1,第32位为符号位,1代表负数,这样的话就必须回退一步,将得到的数右移一位(即2 ^ 30)。 嗯,那么这一部分就先告一段落了。 来看看之前的那些函数。 public boolean add(E e) { addLast(e); return true; } /** * 在队列头部插入元素,如果元素为null,则抛出异常 */ public void addFirst(E e) { if (e == null) throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e; if (head == tail) doubleCapacity(); } /** * 在队列尾部插入元素,如果元素为null,则抛出异常 */ public void addLast(E e) { if (e == null) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity(); } add的几个函数比较简单,在头部或者尾部插入元素,如果直接调用add方法,则是在尾部插入,这时直接在对应位置塞入该元素即可。 elements[tail] = e; 然后把tail记录其后一个位置,如果tail记录的位置已经是数组的最后一个位置了怎么办?嗯,这里又有一个巧妙的操作,跟HashMap中的取模是一样的: tail = (tail + 1) & (elements.length - 1) 因为elements.length是2的幂次方,所以减一后就变成了掩码,tail如果记录的是最后一个位置,即elements.length - 1,tail + 1 则等于elements.length,与elements.length - 1 做与操作后,就变成了0,嗯,没错,这样就变成了一个循环数组,如果tail与head相等,则表示没有剩余空间可以存放更多元素了,则调用doubleCapacity进行扩容: private void doubleCapacity() { assert head == tail; int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1; if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r); System.arraycopy(elements, 0, a, r, p); elements = a; head = 0; tail = n; } 扩容其实也是很简单粗暴的,先记录一下原来head的位置,然后把容量变为原来的两倍,然后把head之后的元素复制到新数组的开头,把剩余的元素复制到新数组之后。以之前的栗子为例,新建的ArrayDeque实例容量为8,然后我们调用add插入元素,插入H之后,tail指向第一个位置,与head重合,就会触发扩容。 arrayDeque.add("A"); arrayDeque.add("B"); arrayDeque.add("C"); arrayDeque.add("D"); arrayDeque.add("E"); arrayDeque.add("F"); arrayDeque.add("G"); arrayDeque.add("H"); arrayDeque.add("I"); 看图应该就比较清楚了,然后来看看获取元素的几个方法: // 获取元素 String a = arrayDeque.getFirst(); String a1 = arrayDeque.pop(); String b = arrayDeque.element(); String b1 = arrayDeque.removeFirst(); String c = arrayDeque.peek(); String c1 = arrayDeque.poll(); String d = arrayDeque.pollFirst(); String i = arrayDeque.pollLast(); String e = arrayDeque.peekFirst(); String h = arrayDeque.peekLast(); String h1 = arrayDeque.removeLast(); System.out.printf("a = %s, a1 = %s, b = %s, b1 = %s, c = %s, c1 = %s, d = %s, i = %s, e = %s, h = %s, h1 = %s", a,a1,b,b1,c,c1,d,i,e,h,h1); System.out.println(); getFirst方法直接取head位置的元素,如果为null则抛出异常。 public E getFirst() { @SuppressWarnings("unchecked") E result = (E) elements[head]; if (result == null) throw new NoSuchElementException(); return result; } getLast也是类似,取出tail所在位置的前一个位置,这里也做了掩码操作。 public E getLast() { @SuppressWarnings("unchecked") E result = (E) elements[(tail - 1) & (elements.length - 1)]; if (result == null) throw new NoSuchElementException(); return result; } element方法直接调用的是getFirst方法: public E element() { return getFirst(); } remove方法有三个: public E remove() { return removeFirst(); } public E removeFirst() { E x = pollFirst(); if (x == null) throw new NoSuchElementException(); return x; } public E removeLast() { E x = pollLast(); if (x == null) throw new NoSuchElementException(); return x; } remove方法其实是调用的对应的poll方法,poll方法也有三个: public E poll() { return pollFirst(); } public E pollFirst() { int h = head; @SuppressWarnings("unchecked") E result = (E) elements[h]; // 如果结果为null则返回null if (result == null) return null; elements[h] = null; // Must null out slot head = (h + 1) & (elements.length - 1); return result; } public E pollLast() { int t = (tail - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[t]; if (result == null) return null; elements[t] = null; tail = t; return result; } 其实也很简单,都是先取出对应的元素,如果为null则返回null,否则取出对应的元素并对head或tail进行调整。 pop方法调用的是removeFirst方法,removeFIrst方法调用的是pollFirst方法,所以方法看起来这么多,实际上最后真正调用的就那么几个。 public E pop() { return removeFirst(); } peek方法是取出元素但是不移除,也有三个方法: public E peek() { return peekFirst(); } @SuppressWarnings("unchecked") public E peekFirst() { // elements[head] is null if deque empty return (E) elements[head]; } @SuppressWarnings("unchecked") public E peekLast() { return (E) elements[(tail - 1) & (elements.length - 1)]; } 这里没有做任何校验,所以如果如果取到的元素是null,返回的也是null。 再来看看插入元素的其它几个方法: public boolean offer(E e) { return offerLast(e); } public boolean offerLast(E e) { addLast(e); return true; } public boolean offerFirst(E e) { addFirst(e); return true; } public void push(E e) { addFirst(e); } offer方法直接调用的是add方法。 emmm,都是相互调用,为啥要设置那么多方法呢?其实主要是为了模拟不同的数据结构,如栈操作:pop,push,peek,队列操作:add,offer,remove,poll,peek,element,双端队列操作:addFirst,addLast,getFirst,getLast,peekFirst,peekLast,removeFirst,removeLast,pollFirst,pollLast。不过确实稍微多了一点。 之前的栗子里还有用到两个方法,removeFirstOccurrence和removeLastOccurrence,前者是移除首次出现的位置,后者是移除最后一次出现的位置。 public boolean removeFirstOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = head; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i + 1) & mask; } return false; } public boolean removeLastOccurrence(Object o) { if (o == null) return false; int mask = elements.length - 1; int i = (tail - 1) & mask; Object x; while ( (x = elements[i]) != null) { if (o.equals(x)) { delete(i); return true; } i = (i - 1) & mask; } return false; } 其实都是通过循环遍历的方式进行查找一个是从head开始往后查找,一个是从tail开始往前查找。 最后,我们再来看看它的迭代器类。 public Iterator<E> iterator() { return new DeqIterator(); } private class DeqIterator implements Iterator<E> { private int cursor = head; private int fence = tail; private int lastRet = -1; public boolean hasNext() { return cursor != fence; } public E next() { if (cursor == fence) throw new NoSuchElementException(); @SuppressWarnings("unchecked") E result = (E) elements[cursor]; if (tail != fence || result == null) throw new ConcurrentModificationException(); lastRet = cursor; cursor = (cursor + 1) & (elements.length - 1); return result; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); if (delete(lastRet)) { cursor = (cursor - 1) & (elements.length - 1); fence = tail; } lastRet = -1; } public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] a = elements; int m = a.length - 1, f = fence, i = cursor; cursor = f; while (i != f) { @SuppressWarnings("unchecked") E e = (E)a[i]; i = (i + 1) & m; if (e == null) throw new ConcurrentModificationException(); action.accept(e); } } } 在迭代器类中,cursor记录的是head的位置,fence记录的是tail的位置,lastRet记录的是调用next返回的元素的序号,如果调用了remove方法,lastRet会置为-1,这里没有像其它容器那样使用modCount来实现fast-fail机制,而是通过在next方法中进行修改判断。 // 如果移除了尾部元素,会导致 tail != fence // 如果移除了头部元素,会导致 result == null if (tail != fence || result == null) throw new ConcurrentModificationException(); 当然,这种检测比较弱,如果先移除一个尾部元素,然后再添加一个尾部元素,那么tail依旧和fence相等,这种情况就检测不出来了。 在调用remove方法的时候,调用了一个delete方法,这是ArrayDeque类中的一个私有方法。 private boolean delete(int i) { // 先做不变性检测,判断是否当前结构满足删除需求 checkInvariants(); final Object[] elements = this.elements; // mask即掩码 final int mask = elements.length - 1; final int h = head; final int t = tail; // front代表i到头部的距离 final int front = (i - h) & mask; // back代表i到尾部的距离 final int back = (t - i) & mask; // 再次校验,如果i到头部的距离大于等于尾部到头部的距离,表示当前队列已经被修改了,通过最开始检测后,i是不应该满足该条件的 if (front >= ((t - h) & mask)) throw new ConcurrentModificationException(); // 为移动尽量少的元素做优化,如果离头部比较近,则将该位置到头部的元素进行移动,如果离尾部比较近,则将该位置到尾部的元素进行移动 if (front < back) { if (h <= i) { System.arraycopy(elements, h, elements, h + 1, front); } else { // Wrap around System.arraycopy(elements, 0, elements, 1, i); elements[0] = elements[mask]; System.arraycopy(elements, h, elements, h + 1, mask - h); } elements[h] = null; head = (h + 1) & mask; return false; } else { if (i < t) { // Copy the null tail as well System.arraycopy(elements, i + 1, elements, i, back); tail = t - 1; } else { // Wrap around System.arraycopy(elements, i + 1, elements, i, mask - i); elements[mask] = elements[0]; System.arraycopy(elements, 1, elements, 0, t); tail = (t - 1) & mask; } return true; } } 所以这个delete还是花了一点心思的,不仅做了两次校验,还对元素移动进行了优化。嗯,到此为止,源码部分就差不多了。 那么现在再回到最开始提的问题。 1、ArrayDeque是什么?ArrayDeque是一个用循环数组实现的双端队列。 2、ArrayDeque如何使用?通过add,offer,poll等方法进行操作。 3、ArrayDeque的内部结构是怎样的?内部结构是一个循环数组。 4、ArrayDeque的各个方法是如何实现的?嗯,见上文。 5、ArrayDeque是如何扩容的?扩容成原来的两倍,然后将原来的内容复制到新数组中。 6、ArrayDeque的容量有什么限制?容量必须为2的幂次方,最小为8,默认为16. 7、ArrayDeque和LinkedList相比有什么优势?ArrayDeque通常来说比LinkedList更高效,因为可以在常量时间通过序号对元素进行定位,并且省去了对元素进行包装的空间和时间成本。 8、ArrayDeque的应用场景是什么?在很多场景下可以用来代替LinkedList,可以用做队列或者栈。 到此,本篇圆满结束。如果觉得还不错的话,记得动动小手点个赞,也欢迎关注博主,你们的支持是我写出更好博客的动力。 有兴趣对Java进行更深入学习和交流的小伙伴,欢迎加入QQ群交流:529253292

优秀的个人博客,低调大师

C语言入门:输入任意一个正数(奇数),判断是否为质数

C语言永远不会过时 其实学编程关键是学习其思想,如果你精通了一门,再去学其他的时候也很容易上手。C不会过时的,尤其是在unix、linux操作平台上,学好C是必须的。 C跟C++在很多方面也是兼容的,c是c++的基础。 再者c能从很大的程度上帮你了解计算机的发展史,数据结构等方面的知识,很多软件、甚至操作系统中的很大部分是用c来实现的。 还有一些电器芯片的程序,比如电冰箱内制冷系统……可以说用c可以解决一切可能遇到的问题,关键是你要能精通它。 所以放开手脚去大胆的学吧,c永远不会过时 C/C++直播学习群:487875004 源代码: #include.h> void main() { int a,b; while(1) { printf("请输入任意正整数(奇数),判断是否为质数:\n"); scanf("%d",&b); if (b==1) printf("1既不是质数,也不是合数。\n\n"); else {for (a=2;a if (b%a==0)break; if(a printf("这个数不是质数\n"); else printf("这个数是质数\n"); printf("\n"); } } }

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册