使用订阅号实现微信公众号历史文章爬虫

微信公众号已经成为生活的一部分了,虽然里面有很多作者只是为了蹭热点,撩读者的 G 点,自己从中获得一些收益;但是不乏好的订阅号,像刘大的码农翻身、Fenng的小道消息、曹大的caoz的梦呓等订阅号非常值得阅读。 平时有时候看到一些好的公众号,也会不自觉去查看该公众号的历史文章,然而每次都看不完,下一次再从微信里面打开历史文章,又需要从头翻起。而且对于写了很多年的大号,每次还翻不到底。有一些平台提供了相关的服务,但是得收几十块钱的费用,倒不是缺几十块钱,主要是觉得这种没必要花的钱不值得去浪费。 网上搜如何爬微信公众号历史文章,大致给了三种思路,第一是使用搜狗微信搜索文章,但是好像每次能搜到的不多;第二是使用抓包工具;第三种是使用个人订阅号进行抓取。 简单来说就是使用程序来模拟人的操作,抓取公众号历史文章。首先登录微信公众号个人平台,期间需要管理员扫码才能登录成功。 def __open_gzh(self): self.driver.get(BASE_URL) self.driver.maximize_window() username_element = self.driver.find_element_by_name("account") password_element = self.driver.find_element_by_name("password") login_btn = self.driver.find_element_by_class_name("btn_login") username_element.send_keys(USERNAME) password_element.send_keys(PASSWORD) login_btn.click() WebDriverWait(driver=self.driver, timeout=200).until( ec.url_contains("cgi-bin/home?t=home/index") ) # 一定要设置这一步,不然公众平台菜单栏不会自动展开 self.driver.maximize_window() 进入微信公众平台首页后,点击素材管理,然后点击新建图文素材,就会进入到文章写作页面,此时前面打开的微信公众平台首页就不需要了,可以将其关闭。 def __open_write_page(self): management = self.driver.find_element_by_class_name("weui-desktop-menu_management") material_manage = management.find_element_by_css_selector("a[title='素材管理']") material_manage.click() new_material = self.driver.find_element_by_class_name("weui-desktop-btn_main") new_material.click() # 关闭公众平台首页 handles = self.driver.window_handles self.driver.close() self.driver.switch_to_window(handles[1]) 在文章写作页面的工具栏上面有一个超链接按钮,点击超链接即会弹出超链接编辑框,选择查找文章,输入自己喜欢的公众号进行查找,一般第一个就是自己想要的结果,点击对应的公众号,该公众号所有的文章就会通过列表的形式展现出来。 def __open_official_list(self): # 超链接 link_click = self.driver.find_element_by_class_name("edui-for-link") link_click.click() time.sleep(3) # 查找文章 radio = self.driver.find_element_by_class_name("frm_vertical_lh").find_elements_by_tag_name("label")[1] radio.click() # 输入查找关键字 search_input = self.driver.find_element_by_class_name("js_acc_search_input") search_input.send_keys(OFFICIAL_ACCOUNT) search_btn = self.driver.find_element_by_class_name("js_acc_search_btn") search_btn.click() # 等待5秒,待公众号列表加载完毕 time.sleep(5) result_list = self.driver.find_element_by_class_name("js_acc_list").find_elements_by_tag_name("div") result_list[0].click() 文章列表已经展现出来了,直接抓取每条文章超链接的信息即可,每抓取完一页就进入下一页,继续抓取文章列表信息,直到所有文章信息都抓取完毕。 def __get_article_list(self): # 等待文章列表加载 time.sleep(5) total_page = self.driver.find_element_by_class_name("search_article_result")\ .find_element_by_class_name("js_article_pagebar").find_element_by_class_name("page_nav_area")\ .find_element_by_class_name("page_num")\ .find_elements_by_tag_name("label")[1].text total_page = int(total_page) articles = [] for i in range(0, total_page-1): time.sleep(5) next_page = self.driver.find_element_by_class_name("search_article_result")\ .find_element_by_class_name("js_article_pagebar").find_element_by_class_name("pagination")\ .find_element_by_class_name("page_nav_area").find_element_by_class_name("page_next") article_list = self.driver.find_element_by_class_name("js_article_list")\ .find_element_by_class_name(" my_link_list").find_elements_by_tag_name("li") for article in article_list: article_info = { "date": article.find_element_by_class_name("date").text, "title": article.find_element_by_tag_name("a").text, "link": article.find_element_by_tag_name("a").get_attribute("href") } articles.append(article_info) next_page.click() return articles 至此,微信公众号历史文章的爬虫已经实现,其实整个过程只不过是用程序来模拟的了人类的操作。需要注意的是,程序不能设置太快,因为微信做了相关限制,所以设太快会在一段时间内无法使用文章查找功能;另外一点是使用选择器选择页面元素的时候,会有一些坑,而且我发现不同账号登录,有很少部分的页面元素虽然直观上是一样的,但是它的 html 代码有细微的差别。 这个小程序会用到selenium库,和chromedriver,前者直接pip install即可,后者自行下载;另外你还需要一个订阅号才行,本文只实现了关键的文章信息抓取,并没有进行文章信息的持久化存储,完整代码在这里。
Read More ~

磁盘到底是怎样工作的?一文理解硬盘结构

数据库系统总会涉及到辅助存储(大多都是磁盘),因为它们能够存储大量需要长期保存的数据,因此我们有必要先了解了解磁盘的相关知识。 根据机械原理,存储器的容量越大其速度就越慢。但是速度越快的存储器,其单位字节的价格就越贵。现代计算机系统可以包含几个不同的可以存储数据的部件,就形成了存储器的层次结构,但是需要注意的是「虚拟内存」是操作系统与操作系统运用机器硬件的产物,它不是存储器的层次之一。 磁盘结构 传统的硬盘盘结构是像下面这个样子的,它有一个或多个盘片,用于存储数据。盘片多采用铝合金材料;中间有一个主轴,所有的盘片都绕着这个主轴转动。一个组合臂上面有多个磁头臂,每个磁头臂上面都有一个磁头,负责读写数据。 磁盘一般有一个或多个盘片。每个盘片可以有两面,即第一个盘片的正面为0面,反面为 1 面;第二个盘片的正面为 2 面......依次类推。磁头的编号也和盘面的编号是一样的,因此有多少个盘面就有多少个磁头。盘面正视图如下图,磁头的传动臂只能在盘片的内外磁道之间移动。因此不管开机还是关机,磁头总是在盘片上面。关机时,磁头停在盘片上面,抖动容易划伤盘面造成数据损失,为了避免这样的情况,所以磁头都是停留在起停区的,起停区是没有数据的。 每个盘片的盘面被划分成多个狭窄的同心圆环,数据就存储在这样的同心圆环上面,我们将这样的圆环称为磁道 (Track)。每个盘面可以划分多个磁道,最外圈的磁道是0号磁道,向圆心增长依次为1磁道、2磁道......磁盘的数据存放就是从最外圈开始的。 根据硬盘的规格不同,磁道数可以从几百到成千上万不等。每个磁道可以存储数 Kb 的数据,但是计算机不必要每次都读写这么多数据。因此,再把每个磁道划分为若干个弧段,每个弧段就是一个扇区 (Sector)。扇区是硬盘上存储的物理单位,现在每个扇区可存储 512 字节数据已经成了业界的约定。也就是说,即使计算机只需要某一个字节的数据,但是也得把这个 512 个字节的数据全部读入内存,再选择所需要的那个字节。 柱面是我们抽象出来的一个逻辑概念,简单来说就是处于同一个垂直区域的磁道称为柱面 ,即各盘面上面相同位置磁道的集合。需要注意的是,磁盘读写数据是按柱面进行的,磁头读写数据时首先在同一柱面内从 0 磁头开始进行操作,依次向下在同一柱面的不同盘面(即磁头上)进行操作,只有在同一柱面所有的磁头全部读写完毕后磁头才转移到下一柱面。因为选取磁头只需通过电子切换即可,而选取柱面则必须通过机械切换。数据的读写是按柱面进行的,而不是按盘面进行,所以把数据存到同一个柱面是很有价值的。 磁盘被磁盘控制器所控制(可控制一个或多个),它是一个小处理器,可以完成一些特定的工作。比如将磁头定位到一个特定的半径位置;从磁头所在的柱面选择一个扇区;读取数据等。 现代硬盘寻道都是采用CHS(Cylinder Head Sector)的方式,硬盘读取数据时,读写磁头沿径向移动,移到要读取的扇区所在磁道的上方,这段时间称为寻道时间(seek time)。因读写磁头的起始位置与目标位置之间的距离不同,寻道时间也不同。磁头到达指定磁道后,然后通过盘片的旋转,使得要读取的扇区转到读写磁头的下方,这段时间称为旋转延迟时间(rotational latencytime)。然后再读写数据,读写数据也需要时间,这段时间称为传输时间(transfer time)。 根据上文的信息,我们可以得出磁盘容量的计算公式为: 硬盘容量 = 盘面数 × 柱面数 × 扇区数 × 512字节 笔试题实战 下面的题目是腾讯某一年校招笔试中的一个题目,题干信息描述为:数据存储在磁盘上的排列方式会影响I/O服务的性能,一个圆环磁道上有10个物理块,10个数据记录R1~R10存放在这个磁道上,记录的安排顺序如下表所示。 物理块 1 2 3 4 5 6 7 8 9 10 逻辑记录 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 假设磁盘的旋转速度为20ms,磁盘当前处在R1的开头处,若系统顺序扫描后将数据放入单缓冲区内,处理数据的时间为4ms(然后再读取下个记录),则处理这10个记录的最长时间是多少? 答案:磁盘会一直朝某个方向旋转,不会因为处理数据而停止。本题要求顺序处理 R1 到 R10,起始位置在 R1,一周是 20ms,共 10 个记录,所以每个记录的读取时间为 2ms。首先读 R1 并处理 R1,读 R1 花 2ms,读好后磁盘处于 R1 的末尾或 R2 的开头,此时处理 R1,需要 4ms,因为磁盘一直旋转,所以 R1 处理好了后磁盘已经转到 R4 的开始了,这时花的时间为 2+4=6ms。这时候要处理 R2,需要等待磁盘从 R5 一直转到 R2 的开始才行,磁盘转动不可反向,所以要经过 8*2ms 才能转到 R1 的末尾,读取 R2 需要 2ms,再处理 R2 需要 4ms,处理结束后磁盘已经转到 R5 的开头了,这时花的时间为 2*8+2+4=22ms。等待磁盘再转到 R3 又要 8*2ms,加上 R3 自身 2ms 的读取时间和 4ms 的处理时间,花的时间也为 22ms,此时磁盘已经转到 R6 的开头了,写到这里,就可以看到规律了,读取并处理后序记录都为 22ms,所以总时间为 6+22*9=204ms。 如何加速对磁盘的访问 对于理解数据库系统系统特别重要的是磁盘被划分为磁盘块(或像操作系统一样称之为页),每个块的大小是 4~64KB。磁盘访问一个磁盘块平均要用 10ms,但是这并不表示某一应用程序将数据请求发送到磁盘控制器后,需要等 10ms 才能得到数据。如果只有一个磁盘,在最坏的情况下,磁盘访问请求的到达个数超过 10ms 一次,那么这些请求就会被无限的阻塞,调度延迟将会变的非常大。因此,我们有必要做一些事情来减少磁盘的平均访问时间。 按柱面组织数据:前这一点在前文已经提到过了。因为寻道时间占平均块访问时间的一半,如果我们选择在一个柱面上连续的读取所有块,那么我们只需要考虑一次寻道时间,而忽略其它时间。这样,从磁盘上读写数据的速度就接近于理论上的传输速率。 使用多个磁盘:如果我们使用多个磁盘来替代一个磁盘,只要磁盘控制器、总线和内存能以 n 倍速率处理数据传输,则使用 n 个磁盘的效果近似于 1 个磁盘执行了 n 次操作。因此使用多个磁盘可以提高系统的性能。 磁盘调度:提高磁盘系统吞吐率的另一个有效方法是让磁盘控制器在若干个请求中选择一个来首先执行,调度大量块请求的一个简单而有效的方法就是电梯算法。回忆一下电梯的运行方式,它并不是严格按先来后到的顺序为乘客服务,而是从建筑物的底层到顶层,然后再返回来。同样,我们把磁盘看作是在做横跨磁盘的扫描,从柱面最内圈到最外圈,然后再返回来,正如电梯做垂直运动一样。 预取数据:在一些应用中,我们是可以预测从磁盘请求块的顺序的。因此我们就可以在需要这些块之前就将它们装入主存。这样做的好处是我们能较好的调度磁盘,比如采用前文的电梯算法来减少访问块所需要的平均时间。 磁盘故障 如果事情都像我们一开始设计的那样进行,那世界肯定会变得特别无聊。磁盘偶尔也会耍耍小脾气,甚至是罢工不干了。比如在读写某个扇区一次尝试没有成功,但是反复尝试后有成功读写了,我们称之为间歇性故障。 一种更为严重的故障形式是,一个或多个二进制位永久的损坏了,所以不管我们尝试多少次都不可能成功,这种故障称之为介质损坏。 另一种相关的错误类型称之为写故障,当我们企图写一个扇区时,既不能正确的写,也不能检索先前写入的扇区,发生这种情况的一种可能原因就是在写过程中断电了。 当然肯定最严重的就是磁盘崩溃,这种故障中,整个磁盘都变为永久不可读,这是多么可怕的事情。 既然会出现上面所述的各种大小故障,那么我们就必须要采取各种措施去应对大大小小的变故,保证系统能正常运行。 规避故障 我们尝试读一个磁盘块,但是该磁盘块的正确内容没有被传送到磁盘控制器中,就是一个间歇性故障发生了。那么问题是控制器如何能判断传入的内容是否正确呢?答案就是使用校验和,即在每个扇区使用若干个附加位。在读出时如果我们发现校验和对数据位不合适,那么我们就知道有错误;如果校验和正确,磁盘读取仍然有很小的可能是不正确的,但是我们可以通过增加趣多校验位来降低读取不正确发生的概率。 此处我们使用奇偶校验来举例,通过设置一个校验位使得二进制集合中 1 的个数总是偶数。比如某个扇区的二进制位序列是 01101000,那么就有奇数个 1,所以奇偶位是 1,这个序列加上它后面的奇偶位,就有 011010001;而如果所给的序列是 11101110,那么奇偶位就是 0。所以每一个加上了奇偶位构成的 9 位序列都有偶数奇偶性。 尽管校验和几乎能正确检测出介质故障或读写故障的存在,但是它却不能帮助我们纠正错误。为了处理这个问题,我们可以在一个或多个磁盘中执行一个被称为稳定存储的策略。通常的思想是,扇区时成对的,每一对代表一个扇区内容 X。我们把代表 X 的扇区对分别称为左拷贝 XL和右拷贝XR。这样实际上就是每个扇区的内容都存储了两份,操作XL失败,那么去操作XR就可以了,更何况我们还在每个扇区中有校验和,把错误的概率就大大降低了。 到现在为止,我们讨论的都是简单的故障,但是如果发生了磁盘崩溃,其中的数据被永久破坏。而且数据没有备份到另一种介质中,对于银行金融系统这将是巨大的灾难,遇到这种情况我们应该怎么办呢? 数据恢复 应对磁盘故障最简单的方式就是镜像磁盘,即我们常说的备份。回忆一下写毕业论文时的做法,那时候大部分同学还不会用版本控制器,所以基本采用每天备份一次数据,并且在文件名称中标注日期,以此来达到备份的效果。 第二种方式是使用奇偶块,比如一个系统中有 3 个磁盘,那么我们再加一个磁盘作为冗余盘。在冗余盘中,第 i 块由所有数据盘的第 i 块奇偶校验位组成。也就是说,所有第 I 块的第 j 位,包括数据盘和冗余盘,在它们中间必须有偶数个 1,冗余盘的作用就是让这个条件为真。 我们举个简单例子,假设快仅由一个字节组成,我们有三个数据盘和一个冗余盘,对应的位序列如下。其中 盘4 为冗余盘,它的位序列是根据前面三个盘计算出来的。 盘 1:11110000 盘 2:10101010 盘 3:00111000 盘 4:01100010 假设现在某个盘崩溃了,那么我们就能根据上面的序列来恢复数据,只需要让每一列 1 的个数为偶数就可以了,但是这种冗余方式也存在很大的不足。 第一个缺陷是,如果是两个盘同时崩溃了,那数据也恢复不出来了。第二个问题在于,虽然读数据只需要一次 I/O 操作即可,但是写数据时就不一样了,因为需要根据其他数据盘来计算冗余盘中的位序列,假设共有 n 个盘,其中一个为冗余盘,所以每次写数据时,都需要进行 n+1 次 I/O 操作(读不被写入的 n-1 个盘,被重写数据盘的一次写,冗余盘的一次写),而 I/O操作又是非常耗时的操作,所以这种方法会大大拖慢系统性能。 另一种方案是没有明显的冗余盘,而是把每个磁盘作为某些块的冗余盘来处理。比如现在有 4 个盘,0 号磁盘将作为编号为 4、8、12 等柱面的冗余,而 1 号磁盘作为编号为 1、5、9 等块的冗余...... 一种更为先进的方式使用海明码来帮助从故障中恢复数据,它在多个磁盘崩溃的情况下也能恢复出数据,也是 RAID 的最高等级,由于本人水平有限,用文字表达不清楚,就不作介绍了,嘿嘿。
Read More ~

学习 Angulr 容易忽略的知识点

参考内容: 《Angulr5 高级编程(第二版)》 函数声明式和表达式 // 第一种:函数声明式 myFunc(); function myFunc(){ ... } // 第二种:函数表达式 myFunc(); let myFunc = function(){ ... } 虽然上面两种函数声明方式在大部分情况下是一样的,第一种可执行,第二种却不可以执行,这是因为浏览器在解析 js 时找到函数声明,并在执行剩余语句之前设置好函数,此过程称为函数提升,但是函数表达式却不会受到提升,因此无法正常工作。 js 不具备多态性 js 重不能创建名称相同但参数不同的两个函数,它不具备这个多态性,比如你定义的函数中有两个形参,调用函数时只传一个参数,第二形参的值就是 undefined ,如果传的参数大于 3 个,那么会自动忽略多余的参数。可以使用下列方法来处理函数定义参数数量和用于调用函数实际参数数量之间不匹配的问题。 // 使用默认参数 let func = function(age, sex='男'){ ... } func(23); // 使用可变长参数 let func = function(age, sex, ...extraArgs){ ... } func(23, '女', '张三', '深圳'); // 最后一个参数是一个数组,任何额外的实参都会被赋给这个数组 let 和 war 的区别 使用 let 和 var 声明变量的区别,使用 let 声明变量会把变量的作用范围限定在它所在的代码区域内。而使用 var 所创建的变量的作用域是它所在的函数。 function func(){ if(false){ var age = 23; } } // 上面的代码会被解析成下面的形式,使用 let 则不会出现这样的结果 function func(){ var age; if(false){ age = 23; } } 相等 == 和恒等 === 以及 连接操作符 + 相等操作符尝试将操作数强制转换为相同的类型,再评估是否相等,实质上相等操作符==是测试二者的值是否相等,而与二者的类型无关;如果要测试值和类型是否都相等则应该用恒等操作符===。 5 == '5' // 结果为 true 5 === '5' // 结果为 false 在 js 中,连接操作符的优先级高于加法操作,也就是说5 + '5'的结果是55。 不同的模块指定方式 import { Name } from "./modules/NameUtil";// 第一种 import { Compont } from "@angular/core";// 第二种 上面两种导入模块的方式有所不同,第一种是相对模块,第二种是非相对导入。第一种告诉的 TypeScript 编译器,该模块所在的位置是相对于包含 import 语句的文件而言;第二种非相对导入,编译器会用 node_modules 文件夹中的 npm 包来解析它。 如果在导入模块时,出现需要导入两个不同模块但是名字却相同的情况,可以使用as关键字给导入的模块取一个别名。 import { Name as otherName } from "./modules/Name";//取别名 还有一种方法是将模块作为对象导入,如下 import 所示,导入 Name 模块的内容,并创建一个名为 otherName 的对象,然后就可以使用该对象的属性了。 import * as otherName from "./modules/NameUtil"; let name = new otherName.Name("Admin", "China");// Name 是 NameUtil 中的类 多类型和类型断言 在 ts 中允许指定多个类型,使用字符|进行分隔。看下面的的方法,其功能是把华氏温度转换为摄氏温度。 // 使用多类型,该函数可以传入 number 和 string 类型的参数 static convertFtoC(temp: number | string): string { /* 尝试使用 <> 声明一个类型断言,将一个对象转换为指定类型,也可以使用 as 关键字实现下列相同的效果 let value: number = (temp as number).toPrecision ? temp as number : parseFloat(temp as string); */ let value: number = (<number>temp).toPrecision ? <number>temp : parseFloat(<string>temp); return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1); } 元组是固定长度的数组,数组的每一项都是指定的类型;可索引类型可以将键与值关联起来,创建类似于 map 的集合。 // 元组 let tuple: [string, string, string]; tuple = ["a", "b", "c"]; // 可索引类型 let cities: {[index: string] : [string, string]} = {}; cities["Beijing"] = ["raining", "2摄氏度"]; 数据绑定 [target]="expr"// 方括号表示单向绑定,数据从表达式流向目标; (target)="expr"// 圆括号表示单向绑定,数据从目标流向表达式,用于处理事件的绑定; [(target)]="expr"// 圆方括号组合表示双向绑定,数据在表达式与目标之间双向流动; {{ expression }}// 字符串插入绑定。 [] 绑定有很多不同的形式,下面介绍不同表现形式的效果。 <!-- 标准属性绑定(dom对象有的属性),将 input 的 value 属性绑定到一个表达式的结果 因为 model.getProduct(1) 可能返回 null ,所以使用模板空条件操作符 ? 浏览返回结果 如果返回不为空,那么将读取 name 属性,否则由 null 合并操作符 || 将结果设置为 None 字符串插入绑定也可以使用这种表达式 --> <input [value]="model.getProduct(1)?.name || 'None'"> <!-- 元素属性绑定,有时候我们需要绑定的属性在 DOMAPI 上面没有 可以使用通过在属性名称前加上 attr 前缀的方式来定义目标 --> <td [attr.colspan]="model.getProducts().length"> {{ model.getProduct(1)?.name || 'None' }} </td> <!-- 还有其他的 ngClass,ngStyle 等绑定,理解大体上和上面差不多 --> 内置指令 <!-- ngIf指令,如果表达式求值结果为 true ,那么 ngIf 将宿主元素机器内容包含在 html 文件中 指令前面的星号表示这是一条微模板指令 组要注意的是,ngIf 会向 html 中添加元素,也会从中删除元素,并非只是显示和隐藏 如果只是控制可见性,可以使用属性绑定挥着样式绑定 --> <div *ngIf="expr"></div> <!-- ngSwitch指令, --> <div [ngSwitch]="expr"> <span *ngSwitchCase="expr"></span> <span *ngSwitchDefault></span> </div> <!-- ngFor指令,见名知意,为数组中的每个对象生成同一组元素 ngFor 指令还支持其他的一系列可赋给变量的值,有如下局部模板变量 index:当前对象的位置 odd:如果当前对象的位置为奇数,那么这个布尔值为 true even:同上相反 first:如果为第一条记录,那么为 true last:同上相反 --> <div *ngFor="let item of expr; let i = index"> {{ i }} </div> <!-- ngTemplateOutlet指令,用于重复模板中的内容块 其用法如下所示,需要给源元素指定一个 id 值 <ng-template #titleTemplate> <h1>我是重复的元素哦</h1> </ng-template> <ng-template [ngTemplateOutlet]="titleTemplate"></ng-template> ...省略若万行 html 代码 <ng-template [ngTemplateOutlet]="titleTemplate"></ng-template> --> <ng-template [ngTemplateOutlet]="myTempl"></ng-template> <!-- 下面两个指令就是见名知意了,不解释 --> <div ngClass="expr"></div> <div ngStyle="expr"></div> 事件绑定 事件绑定使用 (target)="expr",是单向绑定,数据从目标流向表达式,用于响应宿主元素发送的事件。 当浏览器触发一个时间时,它将提供一个对象来描述该事件,对于不同类型的事件有不同类型的事件对象,事件对象被赋给一个名为$event的模板变量,但是所有事件对象都有下面三个属性: type:返回一个 string 值,用于标识已触发事件类型; target:返回触发事件的对象,一般是 html元素对象。 timeStamp:返回事件触发事件的 number 值,用 1970.1.1 毫秒数表示。 下面举几个例子,作为理解帮助使用。 <!-- 当数鼠标在上面移动时,就会触发 mouseover 事件 --> <td *ngFor="let item of getProducts()" (mouseover)="selectedProduct = item.name"></td> <!-- 当用户编辑 input 元素的内容时就会触发 input 事件 --> <input (input)="selectedProduct=$event.target.value" /> <input (keyup)="selectedProduct=product.value" /> <!-- 使用事件过滤,上面的写法按下任何一个键都会触发事件,而下面的写法只有回车事件才会触发事件 --> <input (keyup.enter="selectedProduct=product.value") /> 表单验证 Angular 提供了一套可扩展的系统来验证表单元素的内容,总共可以向 input表元素中添加 4 个属性,每个属性定义一条验证规则,如下所示: required:用于指定必须填写值; minlength:用于指定最小字符数; maxlength:用于指定最大字符数,(不能在表单元素直接使用,因为它与同名的 H5 属性冲突); pattern:该属性用于指定用户填写的值必须匹配正则表达式 <!-- Angular 要求验证的元素必须定义 name 属性 由于 Angular 使用的验证属性和 H5 规范使用的验证属性相同, 所以向表单元素中添加 novalidate 属性,告诉浏览器不要使用原生验证功能 ngSubmit 绑定表单元素的 submit 事件 --> <form novalidate (ngSubmit)="addProduct(newProduct)"> <input class="form-control" name="name" [(ngModel)]="newProduct.name" required minlength="5" pattern="^[A-Za-z]+$" /> <button type="submit">提交</button> </form> Angular 提供了 3 对验证 CSS 类,这些类可以用于样式化表单元素,向用户提供验证反馈,具体说明如下所示。 ng-untouched ng-touched:如果一个元素未被用户访问,就将其加入到 nguntouched 类中;一旦访问就加入到 ngtouched 类中。 ng-prisstine ng-dirty:元素内容没有被改变被加入到 ng-prisstine 类中,否则将其加入到 ng-dirty 类中。 ng-valid ng-invalid:如果满足验证规则定义的条件,就加入到 ng-valid 类中,否则加入到 ng-invalid 类中。 在实际使用过程中,直接定义对应的样式即可,如下所示: <style> input.ng-dirty.ng-invalid{ border: 2px solid red; } input.ng-dirty.ng-valid{ border: 2px solid green; } </style> <form novalidate (ngSubmit)="addProduct(newProduct)"> <input class="form-control" name="name" [(ngModel)]="newProduct.name" required minlength="5" pattern="^[A-Za-z]+$" /> <button type="submit">提交</button> </form> 上面的验证方式无法给用户提供更加具体的信息,用户不知道应该做什么,可以使用 ngModel 指令来访问宿主元素的验证状态,当存在验证错误的时候,使用该指令向用户提供指导性信息。 <form novalidate (ngSubmit)="addProduct(newProduct)"> <input class="form-control" #nameRef="ngModel" name="name" [(ngModel)]="newProduct.name" required minlength="5" pattern="^[A-Za-z]+$" /> <ul class="text-danger list-unstyled" *ngIf="name.dirty && name.invalid"> <li *ngIf="name.errors?required"> you must enter a product name </li> <li *ngIf="name.errors?.pattern"> product name can only contain letters and spases </li> <li *ngIf="name.errors?minlength"> <!-- Angular 表单验证错误描述属性 required:如果属性已被应用于 input 元素,此属性返回 true minlength.requiredLength:返回满足 minlength 属性所需的字符数 minlength.actualLength:返回用户输入的字符数 pattern.requiredPattern:返回使用 pattern 属性指定的正则表达式 pattern.actualValue:返回元素的内容 --> product name must be at least {{ name.errors.minlength.requiredLenth }} characters </li> </ul> <button type="submit">提交</button> </form> 如果在用户尝试提交表单时就显示大量的错误信息,给人的体验感就会很差,所以可以让用户提交表单时再验证整个表单,示例代码如下所示。 export class ProductionCompont { // ...省略若万行代码 formSubmited: boolean = false; submitForm(form: ngForm) { this.formSubmited = true; if(form.valid) { this.addProduct(this.newProduct); this.newProduct = new Product(); form.reset(); this.formSubmited = true; } } } <form novalidate #formRef="ngForm" (ngSubmit)="submitForm(formRef)"> <div *ngIf="formsubmited && formRef.invalid"> there are problems with the form </div> <!-- 禁用提交按钮,验证成功提交按钮才可用 --> <button [disabled]="formSubmited && formRef.valid">提交</button> </form> fromSubmited 属性用于指示表单是否已经提交,并将用于在用户提交整个表单之前阻止表单验证。当用户提交表单时,调用 submitForm 方法,并将 ngForm 对象作为实参传入,ngForm 提供了 reset 方法,该方法可以重置表单的验证状态,使其返回到最初的未访问状态。 更高级的还有使用基于模型的表单验证,可以自行查阅相关资料。 使用 json-server 模拟 web 服务 因为json-server会经常用到,建议使用全局安装命令npm install -g json-server。因为开发后端的同学太慢了,而我们如果要等他们把接口都提供给我们的时候再开发程序的话,那效率就太低了,所以使用 json-server 来模拟后端服务。只需要建好一个 json 文件,比如下面的格式: { "user" : [ { "name" : "张三", "number" : "1234", }, { "name" : "王二", "number" : "5678", } ], "praise": [ {"info":"我是一只小老虎呀!"}, {"info":"我才是大老虎"} ] } 启动服务使用命令json-server [你的 json 文件路径],然后就可以根据提示访问了,你甚至可以使用http://localhost:3000/user?number=5678去过滤数据。这样就能模拟 web 服务,而不必等后端同学的进度了。 解决跨域请求问题 Angular 跨域请求问题可以通过 Angular 自身的代理转发功能解决,在项目文件夹下新建一个 proxy.conf.json 并在其中添加如下内容。 // 可以通过下列配置解决 "/api": { "target": "http://10.9.176.120:8888", } 在启动时使用npm start,或者使用ng serve --proxy-config proxy.conf.json,Anular 中的/api请求就会被转发到 http://10.9.176.120:8888/api,从而解决跨域请求问题。 使用第三方 js 插件 共有三种方式引入第三方插件,第一种很简单,直接在 html 中引入插件就可以了;第二种在angular.json中进行配置;第三种在 ts 文件中使用 import 导入库即可。 // 第一种(需要重启服务) "scripts": ["src/assets/jquery-3.2.1.js","src/assets/jquery.nicescroll.js","src/assets/ion.rangeSlider.js"] // 第二种 <script type="text/javascript" src="assets/jquery-3.2.1.js"></script> <script type="text/javascript" src="assets/jquery.nicescroll.js"></script> // 第三种 import "assets/jquery-3.2.1.js"; import "assets/jquery.nicescroll.js"; import "assets/ion.rangeSlider.js"; 深拷贝与浅拷贝 深拷贝与浅拷贝是围绕引用类型变量说的,其本质区别是不可变性,基本类型是不可变得,而引用类型是可变的。 直接使用赋值操作符,就是浅拷贝,如果对拷贝源进行操作,会直接影响在拷贝目标上,因为这个赋值行为本质是内存地址的赋值,为了获得与拷贝源完全相同但又不会影响彼此的对象就要使用深拷贝。 let objA = { x: 1, y: -1 } let objB = objA; objA.x++; console.log("objA.x:"+objA.x, "objB.x:"+objB.x); //打印结果如下: objA.x : 2 objB.x : 2 Typescript 提供了一种方法来实现引用类型的深拷贝,即Object.assign(target, ...source),此方法接受多个参数,第一个参数为拷贝目标,剩余参数为拷贝源,同名属性会进行覆盖。 let objA = { x: 1, y: -1, c: { d: 1, } } let objB = {}; Object.assign(objB, objA); objA.x++; console.log("objA.x:"+objA["x"], "objB.x:"+objB["x"]); //打印结果如下: objA.x : 2 objB.x : 1 需要注意的是,Typescript 提供的深拷贝方法不能实现嵌套对象的深拷贝,会出现下面的情况。 let objA = { x: 1, y: -1, c: { d: 1, } } let objB = {}; Object.assign(objB, objA); objA.c.d++; console.log("objA.c.d:"+objA["c"].d, "objB.c.d:"+objB["c"].d); //打印结果如下: objA.c.d : 2 objB.c.d : 2 要实现嵌套对象的深拷贝,可以使用 JSON 对象提供的方法,JSON 对象提供了两个方法,分别为:stringify()和parse(),前者将对象 JSON 化,后者将 JSON 对象化,使用这种方式可以实现嵌套深拷贝,但是也有缺点:破坏原型链,不能拷贝属性值为 function 的属性。 let objA = { a: 1, b: { c: 1 } } let objB = JSON.parse(JSON.stringify(objA)); objA.b.c++; console.log("objA.b.c:"+objA.b.c, "objB.b.c:"+objB.b.c); //打印结果如下: objA.b.c:2 objB.b.c:1
Read More ~

读大学的几点建议

前天在朋友圈看到一句话:“学生就是无知、狂妄、垃圾的代名词”,让我思考了很多东西。毕业出来也有快两月了,圈子里还有很多学弟学妹,很多同级的同学也都读研了,这里谈谈自己的感受,应该怎么把大学过好,期望不要太高,我自己很普通,大学没有什么出彩的经历。 很多父母都把孩子的成就与大学挂钩,认为好的大学就是成功的代名词,盲目追求高学历,孩子从小也一直受这些思维的影响,应试能力强的惊人。大家都知道清华北大好,但是你问他哪里好,就回答不上来了,尤其家长,在他们眼里 985 一定比 211 好,211 一定比普通一本好。 现在大学都会给你传达自己乃名门之后的观念,进校首先讲历史,当然都是挑好的讲,然后讲学校的历史成绩单。不得不说,效果非常好,你一和大学生谈论他的学校时,他会给你说出来学校是某某名人所建,学校在哪方面做的非常好,比如导弹是全国第几,造船位居全国前列等等。但是你一问他本人是哪个专业,回答是学数学的、学计算机的...... 每个学校都有恶心的事,我自己的一个经历,学院一拍脑袋,搞个什么本科生导师制,然后就没有然后了,四年总共见了导师一面。期间最可笑的是,也不知道是教育部还是什么部来检查,学生需要交一个导师沟通表上去,导师在哪个办公室都不知道,那大家怎么办,就模仿导师的口吻给自己下评语,第二天全院的导师沟通表都被打回来了,因为大家模仿的口吻不像导师,要求重新造假。 上面的类似情况在大部分学校应该都存在,只不过看谁更可笑,某个学生出事了,学校第一想法不是怎么帮自己的学生解决问题,而是想如何把事情压下去,封锁消息。你会发现很多效率像蜗牛一样的机构,其公关效率却像火箭一样。 现在各个大学的就业率都高的惊人,都不会低于 90%,为啥这么高呢?我也不知道学校是如何统计就业率的,唯一清楚的是,你毕业了,没有签工作,那么辅导员会给你打电话让你随便找个公司把三方协议签了交上去;这算轻的,很多学校是你不交三方协议,就不给你学位证、毕业证,我身边就有好几个随便刻个假章,盖在三方上面,只要交了三方协议的都算就业了的。 我个人认为大学有的课就应该逃,也看到过文章说学生上课不应该带有批判性思维,什么课有用不应该是学生说了算,大学的课程设置都是专家们讨论的结果,现在最不缺的就是专家,什么人生导师一大把,出来之后,你仍然会发现有的课纯属浪费时间。强调一下,逃课不是去打游戏,是为了把时间利用的比在课堂上更有价值,我大学微积分老师也鼓励我们逃课,现在看来那时还是胆子太小,人家鼓励你逃课,还不敢逃,怂。重要的事再说一遍,逃课是去做比上课更有价值的事情。 养成自学的习惯,提高自学能力,自学能力太重要了,而且这个时代自学是很容易的,网上有很多视频教程,比学校老师教的还好,而且也更接近于实战,大学教不了你太多东西,仅仅提供了一个平台,只是平台大小的区别而已。经常会听到学生说某个知识点老师没教,潜台词就是这个知识点我就不应该会,而且理直气壮,让人无语。世人都认为学历最重要,实际上真正重要的是学力。 迷茫的时候就去旅行吧,感受一下不同的文化,见识见识世界的缤纷多彩,你的视野会开阔许多,很多事情必须亲身体验才能感受到它的好处,旅途中你可以结识各种各样的朋友,与他们的思想碰撞,看看其他地方的生活,你可能就不会迷茫了,会找到自己乐趣。 多结识比自己优秀的人,认识正能量的朋友,大学提供了很多机会,优秀的朋友会在不知不觉中改变你,你也会不知不觉变得更优秀。我在出于兴趣和打发时间,没事写写文章,让我意外的是,对我的改变太大了,通过写作让我认识了一些社会上的优秀人士,通过与他们交谈,我的思维方式有很大的改变,学生思维逐渐摒弃。 尽量不要透支,学生没有收入来源,基本都是依靠父母每个月给的生活费,很多学生都使用花呗、白条等产品,而且借贷金额还不少,从理财角度来看,每个月的还款额超过自己收入的三分之一,生活就会有压力,何况学生还是没有收入的群体。没必要为了追求时髦而疯狂购买各种新产品,真高品质生活不应该是科技产品堆砌而成,而应该是由惬意、舒心、成长所构建的。 还是要注重和学院领导、辅导员的关系,这点我是做的最差的,因为我不会拍马屁,看不惯就要说出来,容易得罪人。相信这背后的好处都还是明白一二,什么评奖评优暂且不谈,在保研的时候,这种关系会帮你一个大忙,往大了说就是改变人生的机会。 写完读了一遍,有的观点还是显得偏激,请自行选择吸收,也欢迎批评指正。总得来说,最重要的就是提升自己的认知水平,思维方式很重要,保持终身学习的态度。有的事要敢想,不要给自己的思维设限制,也不要觉得博士硕士有多么了不起,研究生能做的事,本科生照样能做。
Read More ~

跨域请求是什么?如何解决?

参考内容: JavaScript: Use a Web Proxy for Cross-Domain XMLHttpRequest Calls 别慌,不就是跨域么! 跨域资源共享 CORS 详解 AJAX请求和跨域请求详解(原生JS、Jquery) JavaScript跨域总结与解决办法 刚毕业入职,大部分时间还在培训,中间有一段时间的空闲时间,就学习了下 Angular,在学校都是编写的单体应用,所有代码都放在同一个工程下面,到公司使用的是前后端分离了,虽然后端程序也是我自己写的,但是有一些数据是从公司现有接口去拿的,然后就遇到让我纠结了两小时的跨域请求问题,在这里做一个简单的总结输出。 什么是跨域请求 跨域请求问题是浏览器的同源策略造成的,该策略不允许执行其它网站的脚本,是浏览器施加的安全限制。什么是同源?最初是指网页 A 设置的 Cookie 不能被网页 B 打开,包括三个相同:协议、域名、端口。这个同源是从 URL 判断的,不是从 IP 判断的,如果同一个服务器对应连个域名,这两个域名是不同源的。 http://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 非跨域 http://www.nealyang.cn/index.html 调用 http://www.neal.cn/server.php 跨域,主域不同 http://abc.nealyang.cn/index.html 调用 http://def.neal.cn/server.php 跨域,子域名不同 http://www.nealyang.cn:8080/index.html 调用 http://www.nealyang.cn/server.php 跨域,端口不同 https://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 跨域,协议不同 localhost 调用 127.0.0.1 跨域 同源政策的目的是为了保护用户信息的安全,防止恶意网站窃取数据,随着互联网的发展,同源政策更加严格了,下面三种行为都会受到限制。 (1) Cookie、LocalStorage 和 IndexDB 无法读取。 (2) DOM 无法获得。 (3) AJAX 请求不能发送。 所有的现代浏览器都对网络连接进行了安全限制,包括 XMLHttpRequest,如果你的 web 应用程序和其使用的数据在同一个服务器,你不会遇到跨域请求问题。但是当你的 web 应用程序和 web 服务数据不在同一个服务器时,就会被浏览器限制连接了。 常用解决方案     对于跨域请求有很多的解决方案,最常用的解决方案是在你的 web 服务器上面设置代理。在设置代理之前就通过,应用程序直接去请求另一个服务器下的数据;设置代理之后,应用程序从自己的 web 服务器中请求数据,再由代理去请求数据,这样 web 服务器拿到数据之后返回给应用程序即可。从浏览器角度看,就是从同一个服务器拿的数据,并没有进行跨域请求。 通俗易懂的说,你家的宠物狗不会吃别家的食物,因为它担心别人的食物会把自己给药死,所以你的狗狗只管找你要食物,你是它的主人,它绝对相信你,而你可以鉴别别人给的食物是不是安全的。类比,小狗就是浏览器,你就是代理。 Angular 中的解决办法 上面所说的解决方案在开发过程中不方便操作,每新发一个接口都到服务器中去配置一下,不仅麻烦而且效率低下。首先说一下在 Angular 中一个人比较常用的解决方法,默认你在使用angular-cli构建你的项目,我们可以创建一个代理配置文件proxy.conf.json(假设你的后端服务的访问地址为10.121.163.10:8080),代理配置文件如下: { "/api": { "target": "http://10.121.163.10:8080", "secure": false } } 然后修改package.json文件中的启动命令为"start": "ng serve --proxy-config proxy.conf.json",启动项目时使用npm start即可解决跨域请求问题。 上述解决方案仅在开发时使用,你当然可以使用 tomcat、nginx 配置代理,但是这很麻烦,需要打包代码部署,为了保证效率,我们想写完了立刻测试,同时也不想麻烦做后端的同学,在项目发布时,应该把代理配置到服务器中去;修改启动命令也不是必须的,你也可以选择每次使用 ng serve --proxy-config proxy.conf.json命令启动项目;示例代理配置文件内容可以有更多的属性,可以通过网络查阅相关资料。 后端解决办法 我的后端是是用 tornado 实现的,然后我又写了一个单独的页面用于在大屏幕上展示相关数据,没有用 Angular 了,要通过 AJAX请求数据,又怎么解决跨域请求问题呢?这时就需要设置请求头了,让后端允许跨域请求。 这时需要了解一下简单请求和非简单请求了,简单请求就是只发送一次请求的请求;非简单请求会发送数据之前先发一次请求做预检,通过预检后才能再发送一次请求用于数据传输。 更清晰区别,满足下列两大条件的属于简单请求,而非简单请求就是请求方法为PUT或DELETE,或者 Content-Type字段是application/json的请求。 1.请求方法为 GET、POST、HEAD之一 2.HTTP头信息不超出字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type,并且 Content-Type 的值仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain。 对于简单请求,只需要设置一下响应头就可以了。 class TestHandler(tornado.web.RequestHandler): def get(self): self.set_header('Access-Control-Allow-Origin', "*") # 可以把 * 写成具体的域名 self.write('cors get success') 对于复杂请求,需要设置预检方法,如下所示: class CORSHandler(tornado.web.RequestHandler): # 复杂请求方法put def put(self): self.set_header('Access-Control-Allow-Origin', "*") self.write('put success') # 预检方法设置 def options(self, *args, **kwargs): #设置预检方法接收源 self.set_header('Access-Control-Allow-Origin', "*") #设置预复杂方法自定义请求头h1和h2 self.set_header('Access-Control-Allow-Headers', "h1,h2") #设置允许哪些复杂请求方法 self.set_header('Access-Control-Allow-Methods', "PUT,DELETE") #设置预检缓存时间秒,缓存时间内发送请求无需再预检 self.set_header('Access-Control-Max-Age', 10)
Read More ~

讲一个爱情故事,让 HTTPS 简单易懂

参考内容 HTTPS explained with carrier pigeons 充满各种数学证明的密码学是令人头疼的,一听到密码、黑客、攻击等词的时候,就给人一种神秘又高大上的感觉,但除非你真的从事密码学相关工作,否则你并不需要对密码学有多么深刻的理解。 这是一篇适合在饭后的品茶时光中阅读的文章,咱们虚构一个故事来讲解,虽然故事看起来很随性,但是 HTTPS 也是这么工作的。里面有一些术语你也应该听过,因为它们经常出现在技术文献里面。 故事背景 一天,一个男子到河边抓鱼给母亲吃,而河岸的另一头是一大户人家的小姐和她的丫鬟在散步。突然,一个不小心,对面小姐不慎跌入水中,而丫鬟又不会游泳,这可把小丫鬟急的呀!!!正在抓鱼的男子见此状况,来不及脱掉身上的衣物,就像箭一样窜入水中.....想必看客已经猜到了,小姐被救起,男子抱着迷迷糊糊小姐走上岸的过程中,小姐感觉自己像触电了一样,觉得这个男人很安全,只要靠着他,就算天塌下来也不怕,而男子把小姐放下的那一刻,也很不舍,好像把她放下就失去了活下去的希望。 小姐回到家中,给父亲大人说了这件事,父亲很高兴,就叫下人去把这位男子请到家中表示感谢,结果一问,这小伙幼年丧父,现在家中还有病弱的老母亲,连一间屋子都没有,一直和母亲寄住在城外的破庙里面,不过他毕竟救了自己的女儿,父亲让下人拿出了五十两黄金以表谢意,但不允许他和小姐再有任何来往。 .....此处省略五千字。 我们姑且称小姐为小花,称男子为小明,他们不能相见了,但是又备受相思之苦,因此只能通过写信的方式来传达彼此的思念了。 最简单的通信方式 如果小花想给小明写信,那么她可以把写好的信让信鸽给小明送去,小明也可以通过信鸽给小花回信,这样他们就能知道彼此的感情了。 但是很快这种方式出问题了,因为他们都隐约感觉到收到的来信不是对方写的,因为从信件上看,双方都表示不再喜欢彼此。凭借着对彼此的信任,他们才知道是小花的父亲从中阻挠他们。每次他们写的信都被父亲的下人拦下了,然后换上他们事先准备好的信件,目的就是为了让小花和小明断了感情。 HTTP 就是这样的工作方式。 对称加密 小花是博冠古今的人,这怎么能难倒她呢。他们彼此约定,每次写信都加上密码,让信鸽传送的信件是用密文书写的。他们约定的密码是把每个字母的位置向后移动三位,比如 A → D 、 B → E ,如果他们要给对方写一句 "I love you" ,那么实际上信件上面写的就是 "L oryh brx" 。现在就算父亲把信件拦截了,他也不知道里面的内容是什么,而且也没办法修改为有效的内容,因为他不知道密码,现在小花和小明又能给对方写情书了。 这就是对称加密,因为如果你知道如何加密信息,那也能知道如何解密信息。上面所说的加密常称为凯撒密码,在现实生活中,我们使用的密码肯定会更复杂,但是主要思想是一样的。 如何确定密钥 显然对称加密是比较安全的(只有两个人知道密码的情况下)。在凯撒密码中,密码通常是偏移指定位数的字母,我们使用的是偏移三位。 可能你已经发现问题了,在小花和小明开始写信之前,他们就已经没办法相见了,那他们怎么确定密钥呢,如果一开始通过信鸽告诉对方密钥,那父亲就能把信鸽拦下,也能知道他们的密钥,那么父亲也就可以查看他们信件的内容,同时也能修改信件了。 这就是典型的中间人攻击,唯一能解决这个问题的办法就是改变现有的加密方式。 非对称加密 小花想出了更好的办法,当小花想给小明写情书的时候,她将会按照下面的步骤来进行: 小花给小明送一只没有携带任何信件的鸽子; 小明让信鸽带一个没有上锁的空箱子回去,钥匙由小明保管; 小花把写好的情书放到箱子里面,并锁上箱子 小明收到箱子后,用钥匙打开箱子就可以了。 使用这种方式,父亲大人就没办法拦截信鸽了,因为他没有箱子的钥匙。同样如果小明想给小花写情书,也采用这种方式。 这就是非对称加密,之所以称之为非对称加密,是因为即使你能加密信息(锁箱子),但是你却无法解密信息(开箱子),因为箱子的钥匙在对方那里。在技术领域,把这里的箱子称作公钥,把钥匙称作私钥。 认证机构 细心的你可能发现问题了,当小明收到箱子后,他如何确定这个箱子的主人是谁呢,因为父亲也可以让信鸽带箱子给小明啊,所以父亲如果想知道他们的信件内容,那只需要把箱子偷换掉就好了。 小花决定在箱子上面签上自己的名字,因为笔迹是不能模仿的,这样父亲就没办法伪造箱子了。但是依旧有问题,小花和小明在不能相见之前并没有见过彼此写的字,那么小明又如何识别出小花的字迹呢?所以他们的解决办法是,找张三丰替小花签名。 众所周知,张三丰是当世的得道高人,他的品德是世人都认可的,大家都把他奉为圣人,而且天下肯定不止一对有情人遇到小花和小红这样的问题。张三丰只会为合法居民签名。 张三丰会在小花的盒子上签名,前提是他确定了要签名的是小花。所以父亲大人是无法得到张三丰代表小花签名的盒子,否则小明就会知道这是一个骗局,因为张三丰只在验证了人们的身份后才会代表他们给盒子签名。 张三丰在技术领域的角色就是认证机构,你现在阅读这篇文章所使用的浏览器是附带了各种认证机构的签名的。所以当你第一次访问某个网站时,你相信这不是一个钓鱼网站,是因为你相信第三方认证机构,因为他们告诉你这个箱子是合法的。 箱子太重了 虽然现在小花和小明有了一个可靠的通信系统,但是信鸽带个箱子飞的慢啊,热恋中的人是“一日不见如隔三秋”,信鸽飞慢了怎么行呢。 所以他们决定还是采用对称加密的方式来写情书,但是对称加密的密钥要用箱子来传递,也就是用非对称加密方式来传递对称加密密钥,这样就可以同时获得对称加密和非对称加密的优点了,还能避免彼此的缺点。 需要注意的是,在网络世界中,信息不会像鸽子传送的那么慢,只不过只用非对称加密技术加密信息要比对称加密慢,所以只用它来交换密钥。 以上就是 HTTPS 的工作过程。 一个故事 这个故事你可能早就知道了,我只是在写文章的过程中突然想起了它,就是笛卡尔的爱情故事。 具体细节你可以网上去查,笛卡尔每天给自己喜欢的公主写信,但是信都被国王拦截了,笛卡尔给公主写的第十三封信中只有一个数学方程,但是这个方程国王看不懂,所以就把这封信交给了公主,公主一看方程,立刻着手把方程的图形画了出来,发现这是一颗心的形状。
Read More ~

Scrapy 爬虫框架入门——抓取豆瓣电影 Top250

最好的学习方式就是输入之后再输出,分享一个自己学习scrapy框架的小案例,方便快速的掌握使用scrapy的基本方法。 本想从零开始写一个用Scrapy爬取教程,但是官方已经有了样例,一想已经有了,还是不写了,尽量分享在网上不太容易找到的东西。自己近期在封闭培训,更文像蜗牛一样,抱歉。 Scrapy简介 Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。 其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。 如果此前对scrapy没有了解,请先查看下面的官方教程链接。 架构概览:https://docs.pythontab.com/scrapy/scrapy0.24/topics/architecture.html Scrapy入门教程:https://docs.pythontab.com/scrapy/scrapy0.24/intro/tutorial.html 爬虫教程 首先,我们看一下豆瓣TOP250页面,发现可以从中提取电影名称、排名、评分、评论人数、导演、年份、地区、类型、电影描述。 Item对象是种简单的容器,保存了爬取到得数据。其提供了类似于词典的API以及用于声明可用字段的简单语法。所以可以声明Item为如下形式。 class DoubanItem(scrapy.Item): # 排名 ranking = scrapy.Field() # 电影名称 title = scrapy.Field() # 评分 score = scrapy.Field() # 评论人数 pople_num = scrapy.Field() # 导演 director = scrapy.Field() # 年份 year = scrapy.Field() # 地区 area = scrapy.Field() # 类型 clazz = scrapy.Field() # 电影描述 decsription = scrapy.Field() 我们抓取到相应的网页后,需要从网页中提取自己需要的信息,可以使用xpath语法,我使用的是BeautifulSoup网页解析器,经过BeautifulSoup解析的网页,可以直接使用选择器筛选需要的信息。有一些说明写到代码注释里面去了,就不再赘述。 Chrome 也可以直接复制选择器或者XPath,如下图所示。 class douban_spider(Spider): count = 1 # 爬虫启动命令 name = 'douban' # 头部信息,伪装自己不是爬虫程序 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36', } # 爬虫启动链接 def start_requests(self): url = 'https://movie.douban.com/top250' yield Request(url, headers=self.headers) # 处理爬取的数据 def parse(self, response): print('第', self.count, '页') self.count += 1 item = DoubanItem() soup = BeautifulSoup(response.text, 'html.parser') # 选出电影列表 movies = soup.select('#content div div.article ol li') for movie in movies: item['title'] = movie.select('.title')[0].text item['ranking'] = movie.select('em')[0].text item['score'] = movie.select('.rating_num')[0].text item['pople_num'] = movie.select('.star span')[3].text # 包含导演、年份、地区、类别 info = movie.select('.bd p')[0].text director = info.strip().split('\n')[0].split(' ') yac = info.strip().split('\n')[1].strip().split(' / ') item['director'] = director[0].split(': ')[1] item['year'] = yac[0] item['area'] = yac[1] item['clazz'] = yac[2] # 电影描述有为空的,所以需要判断 if len(movie.select('.inq')) is not 0: item['decsription'] = movie.select('.inq')[0].text else: item['decsription'] = 'None' yield item # 下一页: # 1,可以在页面中找到下一页的地址 # 2,自己根据url规律构造地址,这里使用的是第二种方法 next_url = soup.select('.paginator .next a')[0]['href'] if next_url: next_url = 'https://movie.douban.com/top250' + next_url yield Request(next_url, headers=self.headers) 然后在项目文件夹内打开cmd命令,运行scrapy crawl douban -o movies.csv就会发现提取的信息就写入指定文件了,下面是爬取的结果,效果很理想。
Read More ~

谈一下写作的重要性,每个人都应该养成写作的习惯

关于写作的重要性,你可能在其他地方也见过一些描述,大致的说法都差不多,如果本文某些字句与你已经见过的文章有雷同,那纯属巧合,我仅从个人这几个月的感受出发来说。 我从三月份开始,在公众号上面发一些文章,其实从这几个月的表现来看,是把公众号当作博客来用了,我的初衷没有想着靠公众号赚钱,所以我的分享很随意,主要是技术、认知、阅读方面的东西。如果不喜欢,主动权在你手里,大可取关;如果喜欢,我的文章能让你少走一些弯路,那么我自己的目的达到了,给不给赞赏无所谓,其实心里还是希望你给的,咱没必要把自己放的太清高。 今天微信订阅号改版了,新版的订阅号基本形态变成了信息流,关于产品我还不太懂,但是我觉得这次改版对于作者的个人品牌形成不利,没内容的公众号应该会被取关很多粉丝,但是忠实粉丝也更难找到自己喜欢的“博主”了,所以以后,请认准作者。 很多人也有撰写博客的习惯,我很赞同这个做法,关于写作的好处我觉得有以下几点,只有认真去做了,才能体会到它带给自己的成长。 第一,写作是整理自己思维的过程,写作能力是一种重要的能力,不一定要多好的文采,但是不是每个人都能把语言组织的有条有理。现在的时代,没有铁饭碗,你需要不停的学习才能立于不败之地,很多人觉得写作是浪费时间,其实不然,写作是对已学知识的整理过程,输出其实是更高层次的输入。拿我之前写的朴素贝叶斯实现拼写检查器来说,其中那个贝叶斯公式推导是我花了很多分钟才想出来的,就好像老师教给学生一碗水,那么老师就必须具备一桶水才行。 第二,写作是个人品牌的建立过程,可以说微信已经成为了中国互联网的小小代名词,农村大叔大妈手机上面最可能出现的软件就是微信,微信打通中国互联网的最后一环,在这么大的平台上,你分享的内容对别人来说是有帮助的,那么你的个人品牌就已经逐渐在形成了,这是个人影响力的提升。个人品牌在以后一定会很重要,个人品牌在日常生活其实有体现,我相信每个人的微信都会屏蔽几个人的朋友圈信息吧,经常在朋友圈发一些无用信息、垃圾信息,这其实就是个人品牌的损失。 第三,通过写作你能交到很多朋友,而且通过这种方式所交到的朋友都是优秀的,他们会对你的成长起到促进作用,而你也会因为和他们交流而在不知不觉中得到提升,真正的朋友是相互促进的。我这几个月交到的朋友,刷新了我的认知,偏见来源于无知,在这个过程,我的认知得到了很大的提升,认知这玩意也不太好描述。举个例子,大概在大二的时候,我看到一篇文章说高中物理中所学的电子、质子等概念是错的,将要被新的知识体系取代,那时二话不说就转发朋友圈了,但没过几天就发现这其实是一个虚假信息;前段时间,中兴被美国制裁了,然后就有一些自媒体作者为了吸引流量,乱写一通什么华为宣布将要退出美国市场的消息,我的第一直觉就是这是虚假信息,然后我去验证了自己猜测的正确性,而周围很多人竟无脑式的选择了相信这条消息,还给我分析为什么华为要退出美国市场。这在我看来就是认知水平的一个体现,或者贴切一点叫信息素养(这个词不是我发明的),我现在对于信息的掌握已经明显快于周围的同学了,而且掌握的也比周围同学更加全面。 最后说一点,没必要为了写作而写作,经常在知识星球看到有人问问题,说自己的写不出东西来,怎么办?这就是自己的输入不够,自己体内没有实质的东西,如何能达到输出呢?更别说高质量的输出了。 我以后的文章主要是机器学习和提升认知方面的,最近更文有点慢,其一是自己也刚开始接触机器学习不久,要写出一篇比较好的文章,需要几天的输入;其二马上要毕业了,繁忙于各种琐碎的事情无法自拔。 总的来说,写作利大于弊,如果你有闲心,看一下我几个月前发的文章,再和我现在的文章做个对比,你能看到我的变化,在文章逻辑、排版等等方面都或多或少的有一些提升,所以我建议你如果空闲时间比较多,也可以尝试尝试写作,自己的成长过程会在字里行间被记录下来。
Read More ~

动态规划算法优化实例——如何求解换钱的方法数

这是我的人生处女面遇到的一个面试题,是在去哪儿网二面遇到的,那时非常的紧张,还没有复习,所以第一次面试理所应当的挂了。文章对问题进行逐步的由简到难进行优化,基本上是代码,看懂代码才能理解,也为类似问题提供了基本的解决思路。 题目描述: 让你把一张整钱找零,即假设你拥有不同且不限量的小额钱币,你需要统计共有多少种方法可以用手中的小额钱币兑等额兑换一张大额钱币。 即:给定一个元素为正数的集合(元素不重复)代表不同面值的钱币,再给一个整数,代表要找零的钱数,求共有多少种换钱方法? 递归求解 现在有1、5、10元三种面值的纸币,需要找零100元,那么可以做如下分析: 用 0 张 5 元换,剩下的用 1、10 元换,最终方法数为 count0; 用 1 张 5 元换,剩下的用 1、10 元换,最终方法数为 count1; ...... 用 100 张 5 元换,剩下的用 1、10 元换,最终方法数为 count100; 最终的换钱方法总数就为 count0 + count1 + ...... + count100。 根据上面的分析可以写出下面的递归解决方案: public static int coin(int money[], int target){ if (money == null || money.length == 0 || target < 0){ return 0; }else { return slove(money, 0, target); } } // 用money[index, length-1]换钱,返回总的方法数 private static int slove(int money[], int index, int target){ int res = 0; if(index == money.length){ if (target == 0){ res = 1; }else { res = 0; } }else { for (int i = 0; money[index] * i <= target; i++) { res += slove(money, index+1, target-money[index]*i); } } return res; } 优化递归 可以看到,上面的程序在运行时存在大量的重复过程,比如下面两种情况,其后所求结果是一样的。 兑换 100 元,已经使用了 0 张 1 元、1 张 2 元,剩下的用 5 元和 10 元兑换; 兑换 100 元,已经使用了 2 张 1 元、0 张 2 元,剩下的用 5 元和 10 元兑换; 可以发现,这两种情况后面都是求解同一问题,重复的对同一个问题求解,就造成了时间的浪费,因此我们可以考虑将已经计算过的结果存下来,避免重复的计算,所以有下面的优化方案。 public static int coin(int money[], int target){ if (money == null || money.length == 0 || target < 0){ return 0; }else { /** * map[i][j]表示p(i,j)递归回的值 * 其中-1表示该递归过程计算过,但是返回值为0 * 0表示该递归过程还为计算过 */ int map[][] = new int[money.length+1][target+1]; return slove(money, 0, target, map); } } private static int slove(int money[], int index, int target, int map[][]){ int res = 0; if(index == money.length){ if (target == 0){ res = 1; }else { res = 0; } }else { int val = 0; for (int i = 0; money[index] * i <= target; i++) { val = map[index + 1][target - money[index]*i]; if (val != 0){ if (val == -1){ res += 0; }else { res += val; } }else { res += slove(money, index+1, target-money[index]*i, map); } } } if (res == 0){ map[index][target] = -1; }else { map[index][target] = res; } return res; } 动态规划 上面对递归方法的优化已经能看到动态规划的影子了,这是一个二维的动态规划问题,我们定义dp[i][j]的含义为:使用money[0...i]的钱币组成钱数j的方法数。所以可以得出以下面的动态规划解法: public static int coin(int money[], int target){ if (money == null || money.length == 0 || target < 0){ return 0; } int dp[][] = new int[money.length][target+1]; // 第一列表示组成钱数为0的方法数,所以为1 for (int i = 0; i < money.length; i++) { dp[i][0] = 1; } // 第一行表示只使用money[0]一种钱币兑换钱数为i的方法数 // 所以是money[0]的倍数的位置为1,否则为0 for (int i = 1; money[0] * i <= target; i++) { dp[0][money[0] * i] = 1; } for (int i = 1; i < dp.length; i++) { for (int j = 1; j < dp[0].length; j++) { for (int k = 0; j >= money[i] * k; k++) { // dp[i][j]的值即为,用money[0...i-1]的钱 // 组成j减去money[i]的倍数的方法数 dp[i][j] += dp[i-1][j-money[i]*k]; } } } return dp[money.length-1][target]; } 继续优化 可以发现上面的动态规划解法有三层循环,因为是二维的动态规划问题,前两层没办法去掉,但是第三层依旧很耗时间,继续优化可以得到下面的结果。 public static int coin(int money[], int target){ if (money == null || money.length == 0 || target < 0){ return 0; } int dp[][] = new int[money.length][target+1]; for (int i = 0; i < money.length; i++) { dp[i][0] = 1; } for (int i = 1; money[0] * i <= target; i++) { dp[0][money[0] * i] = 1; } for (int i = 1; i < money.length; i++) { for (int j = 1; j <= target; j++) { /** * 通过分析可以发现,dp[i][j]的值由两部分组成 * 1:用money[0...i-1]的钱组成钱数为j的方法数 * 2:用money[0...i]的钱组成钱数为j-money[i]*k(k=1,2,3....)的方法数 * 对于第2种情况,实际上累加的值就是dp[i][j-money[i]] * 所以直接使用dp[i][j-money[i]]即可 */ dp[i][j] = dp[i-1][j]; if (j >= money[i]){ dp[i][j] += dp[i][j-money[i]]; } } } return dp[money.length-1][target]; } 空间压缩 可以看到每次更新dp[i][j],dp[i][j]的值只与前一行和当前行前面的元素有关系,而我们只需要最后的一个结果就行了,那么前面存的元素实际上会造成空间的浪费,进一步可以在空间上进行优化。 我们只需要定义一个一位数组,然后对该数组进行滚动更新就可以了,只要按照合适方向去更新数组,同样能达到上面的效果。 public static int coin(int money[], int target){ if (money == null || money.length == 0 || target < 0){ return 0; } int dp[] = new int[target+1]; // 第一行,只用money[0]兑换钱 // 所以只能兑换为money[0]的倍数,将这些位置置为1 for (int i = 0; money[0]*i <= target; i++) { dp[i] = 1; } for (int i = 1; i < money.length; i++) { for (int j = 1; j <= target; j++) { // 与前一步相比,少了dp[i][j] = dp[i-1][j]; // 因为这里在进行dp[j] += dp[j-money[i]];之前 // dp[j]的值就已经是dp[i-1][j]了 if (j >= money[i]){ dp[j] += dp[j-money[i]]; } } } return dp[target]; } 到这一步就不再有优化空间了,这个问题很值得记录下来,很多笔试、面试题都可以按这个模子进行套,对于只需要最优解的动态规划问题也可以套用上面的空间压缩思路,多总结、多练习总是没有问题的!这个解题思路第一次看到是左程云在牛客网上讲解的,他也写了一本算法相关的书比较不错,叫做程序员代码面试指南,大四、研三、刚入职的新人建议可以买一本读读,对自己编码技能的提升绝对又很大的帮助。
Read More ~

年轻不要给自己设限

初入象牙塔时乘坐了 60 多个小时的火车,后面基本都选择了飞机作为出行交通工具,毕业时再次选择了火车这一交通工具回家,再看一次从东北到西南的沿途风景,无奈火车居然能晚点两小时,这篇文章是在火车上为打发时间写的,希望对您有所帮助。 记得大一入学前,买了一本覃彪喜写的《读大学,究竟读什么》,那时候对于里面有一些观点不赞同,觉得大学这么神圣的地方,怎么被作者写成那样,读完一遍只是抱有一种怀疑的态度,四年之后的今天,我觉得这本书值得一看,大部分内容还是有用的,不过有一些内容还是很偏激的,自己过滤掉就好了。 现在回头看,大学最需要的应该是经历,我也是大三才算明白这个道理吧(这个道理应该不止学生能实用)。我认为本科阶段是容错率最高的阶段,这个时候你干什么都不怕,犯了错也没有什么大碍,最重要的是犯错(不犯错更好)的那个过程。 年轻人做什么都是学习,不要给自己设限,在一无所有的年龄就应该多经历,因为这时候的容错率很高,试错成本低就要勇于试错。(这句话可能之前的文章说过,大同小异的话你也能在别的好文章里面见到) 现在的家长,也包括孩子,大多数喜欢拿一些证书、奖杯出来炫耀,而现在大学里面的个性化保研政策看的就是各种奖项。我更看重的是比赛的过程,但是在学校有一个怪现象:我不想办事,只想你给我挂一个名,到出去比赛的时候,看到所报的项目自己不是第一作者,都不愿意去比赛,觉得是在浪费时间。 我个人在这里面算一股清流了,我很喜欢跟着出去比赛,因为比赛的过程能教会你很多在学校学不到的东西,给不给我奖状无所谓,只要给我报销差旅费就行了,这一点对我这种穷学生来说跟重要,想出去看看世界长长见识,自己又没有钱,学生群体中随随便便就拿出几千块钱的人还是不多,所以这是我找到的最好的长见识的方法了,上大学前连小县城都没出过的我,通过比赛到过佛山、深圳、重庆、日照等地,这对我算是一生的财富。 写到这里,脑子里面满满的全是回忆,发现想说的太多,全写出来可能会上万字,先不写了,以后分开写个系列的也行,下面说几句干货道理吧,过来人的总结。 第一,少拿学校的光环往自己身上套,和你没关系,对于我的学校动不动就拿哈军工说事(中国人都喜欢把自己和名家扯上关系,看起来显得有一些历史文化底蕴),完全是不自信的表现。作为唯一一个首批进入211缺不是985的学校,我觉得学校一直在啃老底。类似的文章还有之前写的谈一点关于名校的话题和刘大写的除去大公司的光环,你还剩点啥? 第二,学校教不了你多少东西,大学阶段和高中阶段最大的区别是,高中有人赶着你学,而且还有人给你指明学习的方向,但是大学没有人告诉你学什么,也没有人赶着你学习,所以培养自学能力和判断选择能力很重要,我个人认为这是大学阶段最应该学习到的东西。 第三,如果大学只学习了课本中的内容,那还不如不上大学,不得不承认,大学课本内容都属于经典中的经典,但是学校的要求太低,所以要自己去练习,而且很多老师所教授的东西属于过时的知识,有的课就应该逃掉,利用这个时间去做更有用的事情。 第四,真诚待人,学生阶段所交的朋友没多少功利性,能交几个铁哥们最好。我个人觉得比较实用的一个看人标准,你只需要看某个人对待其他人是什么样,就大概知道他对你会是什么样了;好比谈恋爱,你不要妄想渣男渣女到你这里就不渣了(小概率事件)。
Read More ~