开发这个Chrome插件,更高效地翻译社区文档
注:本文为产品设想,没有实际的产品实现。
最近在看intellij 插件的文档,没有很好的中文版本。加之长期阅读英文文档时的想法和苦恼,准备开发一个chrome插件,用于辅助翻译社区文档。
本文从一个最简单的工具出发,设想了一下操作原型,四步走,如下:
1 . 打开待翻译页面
2 . 选取翻译段落
选取部分文字后跳出询问是否翻译的按钮,点击进入第3步。
3 . 直接修改文字 或进行标注
第2步中选中文字部分变成一个输入框,直接进行修改。
当然,在进行修改时仍然显示原文,保存翻译后隐藏原文是更好的表现方案。图片只是举例说明。
4 . 保存或导出
保存或导出这里方案也有很多,简单例举几个:
* 直接保存网页(到云端、本地)。保存后打开原文链接可选择是否直接展示翻译
* 导出到本地 pdf
* 导出markdown格式
这里最主要的一点就是,基本保留原来的排版结构。不需要在翻译的同时操心排版之事。
要点
- 保留排版
- 直接修改符合习惯
- 打开原文可展示翻译页面
更多feature
- 翻译建议(包括简单单词、词组翻译,通过翻译库匹配的建议,不同领域的建议)
- 云端翻译库相关feature(通过插件翻译并保存于云端,用户自定义公开与否)
- 协同翻译功能(同一页面、同一站点下)(标注、求助等)
- 专有名词关联
- 书库,文档库
- 语法分析(拿不准的句子,看看类似的翻译例句)
- 社交,这也能社交?可以。不过已经是脱离工具之外的主题了
- 一键发布到问答社区或问答版
- 众包形式(类协同模式)
有兴趣的同学请私信交流。
————————————
9.13 更新
谷歌翻译社区
文章片段化处理,分割成句子,用户只需完成短句翻译
分翻译、验证两类。综合评估翻译质量。
深入理解LayoutInflater.inflate()
原文链接:https://www.bignerdranch.com/blog/understanding-androids-layoutinflater-inflate/
译文链接:http://blog.chengdazhi.com/index.php/110
由于我们很容易习惯公式化的预置代码,有时我们会忽略很优雅的细节。LayoutInflater以及它在Fragment的onCreateView()中填充View的方式带给我的就是这样的感受。这个类用于将XML文件转换成相对应的ViewGroup和控件Widget。我尝试在Google官方文档与网络上其他讨论中寻找有关的说明,而后发现许多人不但不清楚LayoutInflater的inflate()方法的细节,而且甚至在误用它。
这里的困惑很大程度上是因为Google上有关attachToRoot(也就是inflate()方法第三个参数)的文档太模糊:
被填充的层是否应该附在root参数内部?如果是false,root参数只适用于为XML根元素View创建正确的LayoutParams的子类。
其实意思就是:如果attachToRoot是true的话,那第一个参数的layout文件就会被填充并附加在第二个参数所指定的ViewGroup内。方法返回结合后的View,根元素是第二个参数ViewGroup。如果是false的话,第一个参数所指定的layout文件会被填充并作为View返回。这个View的根元素就是layout文件的根元素。不管是true还是false,都需要ViewGroup的LayoutParams来正确的测量与放置layout文件所产生的View对象。
attachToRoot传入true代表layout文件填充的View会被直接添加进ViewGroup,而传入false则代表创建的View会以其他方式被添加进ViewGroup。
让我们就两种情况多举一些例子来更深入的理解。
attachToRoot是True
假设我们在XML layout文件中写了一个Button并指定了宽高为match_parent。
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/custom_button">
</Button>
现在我们想动态地把这个按钮添加进Fragment或Activity的LinearLayout中。如果这里LinearLayout已经是一个成员变量mLinearLayout了,我们只需要通过如下代码达成目标:
inflater.inflate(R.layout.custom_button, mLinearLayout, true);
我们指定了用于填充button的layout资源文件,然后我们告诉LayoutInflater我们想把button添加到mLinearLayout中。这里Button的LayoutParams种类为LinearLayout.LayoutParams。
下面的代码也有同样的效果。LayoutInflater的两个参数的inflate()方法自动将attachToRoot设置为true。
inflater.inflate(R.layout.custom_button, mLinearLayout);
另一种在attachToRoot中传递true的情况是使用自定义View。我们看一个layout文件中根元素有
public class MyCustomView extends LinearLayout {
...
private void init() {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.view_with_merge_tag, this);
}
}
这就是一个很好的使用attachToRoot的例子。这个例子中layout文件没有ViewGroup作为根元素,所以我们指定我们自定义的LinearLayout作为根元素。如果layout文件有一个FrameLayout作为根元素而不是
attachToRoot是False
我们看一下什么时候attachToRoot应该是false。在这种情况下,inflate()方法中的第一个参数所指定的View不会被添加到第二个参数所指定的ViewGroup中。
回忆一下刚才的例子中的Button,我们想通过layout文件添加自定义的Button至mLinearLayout中。当attachToRoot为false时,我们仍可以将Button添加到mLinearLayout中,但是这需要我们自己动手。
Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);
这两行代码与刚才attachToRoot为true时的一行代码等效。通过传入false,我们告诉LayoutInflater我们不暂时还想将View添加到根元素ViewGroup中,意思是我们一会儿再添加。在这个例子中,一会儿再添加就是在inflate()后调用addView()方法。
在将attachToRoot设置为false的例子中,由于要手动添加View进ViewGroup导致代码变多了。将Button添加到LinearLayout中还是用一行代码直接将attachToRoot设置为true简便一些。下面我们看一下什么情况下attachToRoot必须传入false。
每一个RecyclerView的子元素都要在attachToRoot设置为false的情况下填充。这里子View在onCreateViewHolder()中填充。
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);
return new ViewHolder(view);
}
RecyclerView负责决定什么时候展示它的子View,这个不由我们决定。在任何我们不负责将View添加进ViewGroup的情况下都应该将attachToRoot设置为false。
当在Fragment的onCreateView()方法中填充并返回View时,要将attachToRoot设为false。如果传入true,会抛出IllegalStateException,因为指定的子View已经有父View了。你需要指定在哪里将Fragment的View放进Activity里,而添加、移除或替换Fragment则是FragmentManager的事情。
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);
if (fragment == null) {
fragment = new MainFragment();
fragmentManager.beginTransaction().add(R.id.root_viewGroup, fragment).commit();
}
上面代码中root_viewGroup就是Activity中用于放置Fragment的容器,它会作为inflate()方法中的第二个参数被传入onCreateView()中。它也是你在inflate()方法中传入的ViewGroup。FragmentManager会将Fragment的View添加到ViewGroup中,你可不想添加两次。
public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
…
return view;
}
问题是:如果我们不需在onCreateView()中将View添加进ViewGroup,为什么还要传入ViewGroup呢?为什么inflate()方法必须要传入根ViewGroup?
原因是及时不需要马上将新填充的View添加进ViewGroup,我们还是需要这个父元素的LayoutParams来在将来添加时决定View的size和position。
你在网上一定会遇到一些不正确的建议。有些人会建议你如果将attachToRoot设置为false的话直接将根ViewGroup传入null。但是,如果有父元素的话,还是应该传入的。
Lint会警告你不要讲null作为root传入。你的App不会挂掉,但是可能会表现异常。当你的子View没有正确的LayoutParams时,它会自己通过generateDefaultLayoutParams计算。
你可能并不想要这些默认的LayoutParams。你在XML指定的LayoutParams会被忽略。我们可能已经指定了子View要填充父元素的宽度,但父View又wrap_content导致最终的View小很多。
下面是一种没有ViewGroup作为root传入inflate()方法的情况。当为AlertDialog创建自定义View时,还无法访问父元素。
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
View customView = inflater.inflate(R.layout.custom_alert_dialog, null);
...
dialogBuilder.setView(customView);
dialogBuilder.show();
在这种情况下,可以将null作为root ViewGroup传入。后来我发现AlertDialog还是会重写LayoutParams并设置各项参数为match_parent。但是,规则还是在有ViewGroup可以传入时传入它。
避开崩溃、异常表现与误解
希望这篇文章可以帮助你在使用LayoutInflater时避开崩溃、异常表现与误解。下面整理了文章的要点:
- 如果可以传入ViewGroup作为根元素,那就传入它。
- 避免将null作为根ViewGroup传入。
- 当我们不负责将layout文件的View添加进ViewGroup时设置attachToRoot参数为false。
- 不要在View已经被添加进ViewGroup时传入true。
- 自定义View时很适合将attachToRoot设置为true。
使用LeanCloud服务做一站式Chrome插件开发——Favorite Image
0. 目录
- 要开发的是什么项目 1.1 想法开端 1.2 应该有什么功能?
- 开发需要解决的核心问题
- 具体解决方案
3.1 帐号系统
3.2 存储服务
3.3 使用
LeanEngine
做反防盗链中转接口 3.4 Chrome 插件实现 - 对去后端化的看法
1. 要开发的是什么项目?
一个Chrome插件,用来保存浏览网页时看到的喜欢的图片。
1.1 想法开端
在 pixiv 翻图时看到一些喜欢的插画,看完就随手翻过去了,没有保存。为什么呢? 因为以我对自己的了解,图片下载下来,就相当于放进了垃圾桶。 并不是因为本地的文件管理有多乱,而是因为,几乎没有用鼠标打开文件管理器的习惯。
现在我获取信息的流量入口最常用的只有两个:1. 终端 2. 浏览器
于是乎,一个想法油然而生:
把插画存到浏览器吧!
于是就立刻构思,动手写了这款插件。
1.2 应该有什么功能?
功能很简单,
保存操作:1. 对图片点击右键 2. 选择"保存到浏览器.." 之类的选项
查看操作:1. 点击插件图标 查看保存过的图片。
其它:1. 图片同步到云端,也可保存到浏览器本地。2. 既然要保存到云端,自然需要账号系统
2. 开发需要解决的核心问题
核心问题有两个,一个是数据云存储问题,一个是图片防盗链问题。
云存储问题,帐号系统,多端同步
最开始只想做浏览器本地的存储,使用Chrome
提供的localStorage
存在本地就。
后来因为localStorage
并不支持数据库语法查询,有很多不便。使用过程中又发现多端同步在体验上的优越性,决定要把存储放到云上。
图片防盗链问题
看了些资料,解决方式基本可以分为两种。
一类使用前端js嵌入iframe
解决,优点是解决方式简单,问题是Chrome
插件不支持页面嵌入式的js
脚本。所以这个方案pass。
第二类使用后台服务器做反防盗链措施,作为中转给前端使用。优点是不受chrome
插件的各种安全机制的限制,缺点是需要后台支持,增加工作量和资源成本。
使用第二类完成。
3. 具体解决方案
云存储及帐号系统使用
LeanCloud
提供的存储服务
解决。
反防盗链接口使用LeanCloud
提供的云引擎
搭建NodeJs
后台。
啰嗦一句,为什么要使用LeanCloud
?
一是对我的需求可以做到完全免费,二是它们的文档实在是太xx的好用了。
3.1 帐号系统
实现过程基本照抄这个教程的代码。后台账号系统包括对账号的重复检测、密码加密、session等都已经实现。
我们要做的,就是调用前端的这几个关键方法,实现简单的注册、登陆、退出:
// LeanCloud - 注册
// https://leancloud.cn/docs/leanstorage_guide-js.html#注册
var user = new AV.User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
user.signUp().then(function (loginedUser) {
// 注册成功
}, (function (error) {
alert(JSON.stringify(error));
}));
// LeanCloud - 登录
// https://leancloud.cn/docs/leanstorage_guide-js.html#用户名和密码登录
AV.User.logIn(username, password).then(function (loginedUser) {
// 登录成功
}, function (error) {
alert(JSON.stringify(error));
});
// LeanCloud - 当前用户信息
// https://leancloud.cn/docs/leanstorage_guide-js.html#当前用户
var currentUser = AV.User.current();
// 退出登陆
AV.User.logOut();
3.2 存储服务
使用账号系统为每个用户添加身份信息后,存储部分就只需要把
数据
+用户身份信息
一同上传或下载就可以了。
照样只贴关键方法
// 初始化类(在数据库中表现为数据表`ImageRepo`)和实例(数据库中表现为一条数据)
this.ImageRepo = AV.Object.extend('ImageRepo');
var repo = new this.ImageRepo();
// 填充数据
repo.put('username', 'xxx');
// 上传数据
repo.save().then(function (repo) {
}, function (error) {
});
// 下载数据
// 初始化对'ImageRepo'表的查询
var query = new AV.Query('ImageRepo');
// 查询条件为 username字段等于'xxx'
query.equalTo('username', 'xxx');
// 查询
query.find().then(function(results) {
// 遍历results
// 在页面添加解决防盗链问题后的图片
}, function(error) {
});
3.3 使用LeanEngine
做反防盗链中转接口
要实现的效果是:
1. 我有一个防盗链图片连接abc.com/xxx.png
。
2. 我的接口url是http://codeli.leanapp.cn/image?url=xxx
。
3. 访问http://codeli.leanapp.cn/image?url=abc.com/xxx.png
可访问原图,不受防盗链措施限制。
主要原理很简单,后台处理图片请求时更改
header
中的referer
字段,将结果作为response
返回。
关于这部分的实现,欢迎阅读我的另一篇文章,就不再赘述:
服务器作防盗链图片中转,nodejs 上手项目简明教程
关于
LeanEngin
的使用,文档如下,使用方法非常简单。
云引擎快速入门云引擎支持
NodeJS
Python
PHP
JAVA
只需要下载云引擎命令行工具lean
,然后输入几行命令就可以建立一个你熟悉的web框架。
然后,使用你熟悉的语言编写反防盗链实现就行了。
3.4 Chrome 插件实现
有了 3.1~3.3 的实现,这部分就是简单的插件部署和业务逻辑了。
Chrome 插件结构如图:
主要业务:
1. 在popup
窗口中添加注册
登陆
退出
等业务。
2. 打开popup
窗口时从云端获取指定账号下保存的图片信息,并展示。若未登陆,则从浏览器localStorage
获取并展示。
3. background script
中添加右键菜单项: 当目标是图片时,显示Keep image in browser
。
4. 点击Keep image in browser
, 执行保存业务逻辑: 若登陆了,保存到云端。若未登录,保存到浏览器localStorage
。
具体实现见我的github项目: KeepImageInBrowser
插件Web Store地址: Favorite Image
4. 最后,对去后端化的看法
前段时间在知乎上看到了一个问题,我也顺便说下自己的看法。
web后端会不会变得越来越不需要?
像bmob和leancloud这类后台云服务的流行有一段日子了,使用这些服务使一些web、app的开发周期大大缩减。这对于小团队和初创公司尤为方便。
但这并不意味着不再需要自己开发后台。不是因为他们提供的服务不够全面(相反,我倒认为这类服务将向着全面、便捷、快速发展),而是因为很多公司和产品,为了保持服务的质量和稳定,突出自己产品的特性,需要自己定制自己的后台,有针对性的去优化某些模块。
云服务作为大众服务平台难以为每个产品做定制。
类似于游戏引擎,如今各个平台都不缺乏优秀的游戏引擎。可是仍有公司和团队耗费大量的成本自研游戏引擎,就是希望能配合自己的游戏系统,完美地展现自己的游戏。
一样的,后台云服务和自定制的后台,是相交但永远不会重合的关系。 他们彼此之间相互影响,共同进步。
如何在Java中避免equals方法的隐藏陷阱
今天看资料时看到coolshell的这篇文章,获益匪浅,收藏到文库里。
译文原文:http://coolshell.cn/articles/1051.html
英文原文:http://www.artima.com/lejava/articles/equality.html
译者注 :你可能会觉得Java很简单,Object的equals实现也会非常简单,但是事实并不是你想象的这样,耐心的读完本文,你会发现你对Java了解的是如此的少。如果这篇文章是一份Java程序员的入职笔试,那么不知道有多少人会掉落到这样的陷阱中。原文转自http://www.artima.com/lejava/articles/equality.html 三位作者都是不同领域的大拿,有兴趣的读者可以从上面这个连接直接去阅读原文。
摘要
本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性。
在《Effective Java》的第8项中,Josh Bloch描述了当继承类作为面向对象语言中的等价关系的基础问题,要保证派生类的equal正确性语义所会面对的困难。Bloch这样写到:
除非你忘记了面向对象抽象的好处,否则在当你继承一个新类或在类中增加了一个值组件时你无法同时保证equal的语义依然正确
在《Programming in Scala》中的第28章演示了一种方法,这种方法允许即使继承了新类,增加了新的值组件,equal的语义仍然能得到保证。虽然在这本书中这项技术是在使用Scala类环境中,但是这项技术同样可以应用于Java定义的类中。在本文中的描述来自于Programming in Scala中的文字描述,但是代码被我从scala翻译成了Java
常见的等价方法陷阱
java.lang.Object 类定义了equals这个方法,它的子类可以通过重载来覆盖它。不幸的是,在面向对象中写出正确的equals方法是非常困难的。事实上,在研究了大量的Java代码后,2007 paper的作者得出了如下的一个结论:
几乎所有的equals方法的实现都是错误的!
这个问题是因为等价是和很多其他的事物相关联。例如其中之一,一个的类型C的错误等价方法可能意味着你无法将这个类型C的对象可信赖的放入到容器中。比如说,你有两个元素elem1和elem2他们都是类型C的对象,并且他们是相等,即elem1.equals(elm2)
返回ture。但是,只要这个equals方法是错误的实现,那么你就有可能会看见如下的一些行为:
Set hashSet<C> = new java.util.HashSet<C>();
hashSet.add(elem1);
hashSet.contains(elem2); // returns false!</pre>
当equals重载时,这里有4个会引发equals行为不一致的常见陷阱:
- 定义了错误的equals方法签名(signature) Defining equals with the wrong signature.
- 重载了equals的但没有同时重载hashCode的方法。 Changing equals without also changing hashCode.
- 建立在会变化字域上的equals定义。 Defining equals in terms of mutable fields.
- 不满足等价关系的equals错误定义 Failing to define equals as an equivalence relation.
在剩下的章节中我们将依次讨论这4中陷阱。
陷阱1:定义错误equals方法签名(signature)
考虑为下面这个简单类Point增加一个等价性方法:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// ...
}
看上去非常明显,但是按照这种方式来定义equals就是错误的。
// An utterly wrong definition of equals
public boolean equals(Point other) {
return (this.getX() == other.getX() && this.getY() == other.getY());
}
这个方法有什么问题呢?初看起来,它工作的非常完美:
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point q = new Point(2, 3);
System.out.println(p1.equals(p2)); // prints true
System.out.println(p1.equals(q)); // prints false
然而,当我们一旦把这个Point类的实例放入到一个容器中问题就出现了:
import java.util.HashSet;
HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);
System.out.println(coll.contains(p2)); // prints false
为什么coll中没有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等价的对象吗?在下面的程序中,我们可以找到其中的一些原因,定义p2a是一个指向p2的对象,但是p2a的类型是Object而非Point类型:
Object p2a = p2;
现在我们重复第一个比较,但是不再使用p2而是p2a,我们将会得到如下的结果:
System.out.println(p1.equals(p2a)); // prints false
到底是那里出了了问题?事实上,之前所给出的equals版本并没有覆盖Object类的equals方法,因为他的类型不同。下面是Object的equals方法的定义
public boolean equals(Object other)
因为Point类中的equals方法使用的是以Point类而非Object类做为参数,因此它并没有覆盖Object中的equals方法。而是一种变化了的重载。在Java中重载被解析为静态的参数类型而非运行期的类型,因此当静态参数类型是Point,Point的equals方法就被调用。然而当静态参数类型是Object时,Object类的equals就被调用。因为这个方法并没有被覆盖,因此它仍然是实现成比较对象标示。这就是为什么虽然p1和p2a具有同样的x,y值,”p1.equals(p2a)”仍然返回了false。这也是会什么HasSet的contains方法返回false的原因,因为这个方法操作的是泛型,他调用的是一般化的Object上equals方法而非Point类上变化了的重载方法equals
一个更好但不完美的equals方法定义如下:
// A better definition, but still not perfect
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
现在equals有了正确的类型,它使用了一个Object类型的参数和一个返回布尔型的结果。这个方法的实现使用instanceof操作和做了一个造型。它首先检查这个对象是否是一个Point类,如果是,他就比较两个点的坐标并返回结果,否则返回false。
陷阱2:重载了equals的但没有同时重载hashCode的方法
如果你使用上一个定义的Point类进行p1和p2a的反复比较,你都会得到你预期的true的结果。但是如果你将这个类对象放入到HashSet.contains()方法中测试,你就有可能仍然得到false的结果:
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);
System.out.println(coll.contains(p2)); // 打印 false (有可能)
事实上,这个个结果不是100%的false,你也可能有返回ture的经历。如果你得到的结果是true的话,那么你试试其他的坐标值,最终你一定会得到一个在集合中不包含的结果。导致这个结果的原因是Point重载了equals却没有重载hashCode。
注意上面例子的的容器是一个HashSet,这就意味着容器中的元素根据他们的哈希码被被放入到”哈希桶 hash buckets”中。contains方法首先根据哈希码在哈希桶中查找,然后让桶中的所有元素和所给的参数进行比较。现在,虽然最后一个Point类的版本重定义了equals方法,但是它并没有同时重定义hashCode。因此,hashCode仍然是Object类的那个版本,即:所分配对象的一个地址的变换。所以p1和p2的哈希码理所当然的不同了,甚至是即时这两个点的坐标完全相同。不同的哈希码导致他们具有极高的可能性被放入到集合中不同的哈希桶中。contains方法将会去找p2的哈希码对应哈希桶中的匹配元素。但是大多数情况下,p1一定是在另外一个桶中,因此,p2永远找不到p1进行匹配。当然p2和p2也可能偶尔会被放入到一个桶中,在这种情况下,contains的结果就为true了。
最新一个Point类实现的问题是,它的实现违背了作为Object类的定义的hashCode的语义。
如果两个对象根据equals(Object)方法是相等的,那么在这两个对象上调用hashCode方法应该产生同样的值
事实上,在Java中,hashCode和equals需要一起被重定义是众所周知的。此外,hashCode只可以依赖于equals依赖的域来产生值。对于Point这个类来说,下面的的hashCode定义是一个非常合适的定义。
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
这只是hashCode一个可能的实现。x域加上常量41后的结果再乘与41并将结果在加上y域的值。这样做就可以以低成本的运行时间和低成本代码大小得到一个哈希码的合理的分布(译者注:性价比相对较高的做法)。
增加hashCode方法重载修正了定义类似Point类等价性的问题。然而,关于类的等价性仍然有其他的问题点待发现。
陷阱3:建立在会变化字段上的equals定义
让我们在Point类做一个非常微小的变化
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) { // Problematic
this.x = x;
}
public void setY(int y) {
this.y = y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
唯一的不同是x和y域不再是final,并且两个set方法被增加到类中来,并允许客户改变x和y的值。equals和hashCode这个方法的定义现在是基于在这两个会发生变化的域上,因此当他们的域的值改变时,结果也就跟着改变。因此一旦你将这个point对象放入到集合中你将会看到非常神奇的效果。
Point p = new Point(1, 2);
HashSet<Point> coll = new HashSet<Point>();
coll.add(p);
System.out.println(coll.contains(p)); // 打印 true
现在如果你改变p中的一个域,这个集合中还会包含point吗,我们将拭目以待。
p.setX(p.getX() + 1);
System.out.println(coll.contains(p)); // (有可能)打印 false
看起来非常的奇怪。p去那里去了?如果你通过集合的迭代器来检查p是否包含,你将会得到更奇怪的结果。
Iterator<Point> it = coll.iterator();
boolean containedP = false;
while (it.hasNext()) {
Point nextP = it.next();
if (nextP.equals(p)) {
containedP = true;
break;
}
}
System.out.println(containedP); // 打印 true
结果是,集合中不包含p,但是p在集合的元素中!到底发生了什么!当然,所有的这一切都是在x域的修改后才发生的,p最终的的hashCode是在集合coll错误的哈希桶中。即,原始哈希桶不再有其新值对应的哈希码。换句话说,p已经在集合coll的是视野范围之外,虽然他仍然属于coll的元素。
从这个例子所得到的教训是,当equals和hashCode依赖于会变化的状态时,那么就会给用户带来问题。如果这样的对象被放入到集合中,用户必须小心,不要修改这些这些对象所依赖的状态,这是一个小陷阱。如果你需要根据对象当前的状态进行比较的话,你应该不要再重定义equals,应该起其他的方法名字而不是equals。对于我们的Point类的最后的定义,我们最好省略掉hashCode的重载,并将比较的方法名命名为equalsContents,或其他不同于equals的名字。那么Point将会继承原来默认的equals和hashCode的实现,因此当我们修改了x域后p依然会呆在其原来在容器中应该在位置。
陷阱4:不满足等价关系的equals错误定义
Object中的equals的规范阐述了equals方法必须实现在非null对象上的等价关系:
- 自反原则:对于任何非null值X,表达式x.equals(x)总返回true。
- 等价性:对于任何非空值x和y,那么当且仅当y.equals(x)返回真时,x.equals(y)返回真。
- 传递性:对于任何非空值x,y,和z,如果x.equals(y)返回真,且y.equals(z)也返回真,那么x.equals(z)也应该返回真。
- 一致性:对于非空x,y,多次调用x.equals(y)应该一致的返回真或假。提供给equals方法比较使用的信息不应该包含改过的信息。
- 对于任何非空值x,x.equals(null)应该总返回false.
Point类的equals定义已经被开发成了足够满足equals规范的定义。然而,当考虑到继承的时候,事情就开始变得非常复杂起来。比如说有一个Point的子类ColoredPoint,它比Point多增加了一个类型是Color的color域。假设Color被定义为一个枚举类型:
public enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
}
ColoredPoint重载了equals方法,并考虑到新加入color域,代码如下:
public class ColoredPoint extends Point { // Problem: equals not symmetric
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
return result;
}
}
这是很多程序员都有可能写成的代码。注意在本例中,类ColoredPointed不需要重载hashCode,因为新的ColoredPoint类上的equals定义,严格的重载了Point上equals的定义。hashCode的规范仍然是有效,如果两个着色点(colored point)相等,其坐标必定相等,因此它的hashCode也保证了具有同样的值。
对于ColoredPoint类自身对象的比较是没有问题的,但是如果使用ColoredPoint和Point混合进行比较就要出现问题。
Point p = new Point(1, 2);
ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);
System.out.println(p.equals(cp)); // 打印真 true
System.out.println(cp.equals(p)); // 打印假 false
“p等价于cp”的比较这个调用的是定义在Point类上的equals方法。这个方法只考虑两个点的坐标。因此比较返回真。在另外一方面,“cp等价于p”的比较这个调用的是定义在ColoredPoint类上的equals方法,返回的结果却是false,这是因为p不是ColoredPoint,所以equals这个定义违背了对称性。
违背对称性对于集合来说将导致不可以预期的后果,例如:
Set<Point> hashSet1 = new java.util.HashSet<Point>();
hashSet1.add(p);
System.out.println(hashSet1.contains(cp)); // 打印 false
Set<Point> hashSet2 = new java.util.HashSet<Point>();
hashSet2.add(cp);
System.out.println(hashSet2.contains(p)); // 打印 true
因此虽然p和cp是等价的,但是contains测试中一个返回成功,另外一个却返回失败。
你如何修改equals的定义,才能使得这个方法满足对称性?本质上说有两种方法,你可以使得这种关系变得更一般化或更严格。更一般化的意思是这一对对象,a和b,被用于进行对比,无论是a比b还是b比a 都返回true,下面是代码:
public class ColoredPoint extends Point { // Problem: equals not transitive
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
else if (other instanceof Point) {
Point that = (Point) other;
result = that.equals(this);
}
return result;
}
}
在ColoredPoint中的equals的新定义比老定义中检查了更多的情况:如果对象是一个Point对象而不是ColoredPoint,方法就转变为Point类的equals方法调用。这个所希望达到的效果就是equals的对称性,不管”cp.equals(p)”还是”p.equals(cp)”的结果都是true。然而这种方法,equals的规范还是被破坏了,现在的问题是这个新等价性不满足传递性。考虑下面的一段代码实例,定义了一个点和这个点上上两种不同颜色点:
ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);
redP等价于p,p等价于blueP
System.out.println(redP.equals(p)); // prints true
System.out.println(p.equals(blueP)); // prints true
然而,对比redP和blueP的结果是false:
System.out.println(redP.equals(blueP)); // 打印 false
因此,equals的传递性就被违背了。
使equals的关系更一般化似乎会将我们带入到死胡同。我们应该采用更严格化的方法。一种更严格化的equals方法是认为不同类的对象是不同的。这个可以通过修改Point类和ColoredPoint类的equals方法来达到。你能增加额外的比较来检查是否运行态的这个Point类和那个Point类是同一个类,就像如下所示的代码一样:
// A technically valid, but unsatisfying, equals method
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY()
&& this.getClass().equals(that.getClass()));
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
你现在可以将ColoredPoint类的equals实现用回刚才那个不满足对称性要的equals实现了。
public class ColoredPoint extends Point { // 不再违反对称性需求
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
return result;
}
}
这里,Point类的实例只有当和另外一个对象是同样类,并且有同样的坐标时候,他们才被认为是相等的,即意味着 .getClass()返回的是同样的值。这个新定义的等价关系满足了对称性和传递性因为对于比较对象是不同的类时结果总是false。所以着色点(colored point)永远不会等于点(point)。通常这看起来非常合理,但是这里也存在着另外一种争论——这样的比较过于严格了。
考虑我们如下这种稍微的迂回的方式来定义我们的坐标点(1,2)
Point pAnon = new Point(1, 1) {
@Override public int getY() {
return 2;
}
};
pAnon等于p吗?答案是假,因为p和pAnon的java.lang.Class对象不同。p是Point,而pAnon是Point的一个匿名派生类。但是,非常清晰的是pAnon的确是在坐标1,2上的另外一个点。所以将他们认为是不同的点是没有理由的。
canEqual 方法
到此,我们看其来似乎是遇到阻碍了,存在着一种正常的方式不仅可以在不同类继承层次上定义等价性,并且保证其等价的规范性吗?事实上,的确存在这样的一种方法,但是这就要求除了重定义equals和hashCode外还要另外的定义一个方法。基本思路就是在重载equals(和hashCode)的同时,它应该也要要明确的声明这个类的对象永远不等价于其他的实现了不同等价方法的超类的对象。为了达到这个目标,我们对每一个重载了equals的类新增一个方法canEqual方法。这个方法的方法签名是:
public boolean canEqual(Object other)
如果other 对象是canEquals(重)定义那个类的实例时,那么这个方法应该返回真,否则返回false。这个方法由equals方法调用,并保证了两个对象是可以相互比较的。下面Point类的新的也是最终的实现:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result =(that.canEqual(this) && this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
public boolean canEqual(Object other) {
return (other instanceof Point);
}
}
这个版本的Point类的equals方法中包含了一个额外的需求,通过canEquals方法来决定另外一个对象是否是是满足可以比较的对象。在Point中的canEqual宣称了所有的Point类实例都能被比较。
下面是ColoredPoint相应的实现
public class ColoredPoint extends Point { // 不再违背对称性
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (that.canEqual(this) && this.color.equals(that.color) && super.equals(that));
}
return result;
}
@Override public int hashCode() {
return (41 * super.hashCode() + color.hashCode());
}
@Override public boolean canEqual(Object other) {
return (other instanceof ColoredPoint);
}
}
在上显示的新版本的Point类和ColoredPoint类定义保证了等价的规范。等价是对称和可传递的。比较一个Point和ColoredPoint类总是返回false。因为点p和着色点cp,“p.equals(cp)返回的是假。并且,因为cp.canEqual(p)总返回false。相反的比较,cp.equals(p)同样也返回false,由于p不是一个ColoredPoint,所以在ColoredPoint的equals方法体内的第一个instanceof检查就失败了。
另外一个方面,不同的Point子类的实例却是可以比较的,同样没有重定义等价性方法的类也是可以比较的。对于这个新类的定义,p和pAnon的比较将总返回true。下面是一些例子:
Point p = new Point(1, 2);
ColoredPoint cp = new ColoredPoint(1, 2, Color.INDIGO);
Point pAnon = new Point(1, 1) {
@Override public int getY() {
return 2;
}
};
Set<Point> coll = new java.util.HashSet<Point>();
coll.add(p);
System.out.println(coll.contains(p)); // 打印 true
System.out.println(coll.contains(cp)); // 打印 false
System.out.println(coll.contains(pAnon)); // 打印 true
这些例子显示了如果父类在equals的实现定义并调用了canEquals,那么开发人员实现的子类就能决定这个子类是否可以和它父类的实例进行比较。例如ColoredPoint,因为它以”一个着色点永远不可以等于普通不带颜色的点重载了” canEqual,所以他们就不能比较。但是因为pAnon引用的匿名子类没有重载canEqual,因此它的实例就可以和Point的实例进行对比。
canEqual方法的一个潜在的争论是它是否违背了Liskov替换准则(LSP)。例如,通过比较运行态的类来实现的比较技术(译者注: canEqual的前一版本,使用.getClass()的那个版本),将导致不能定义出一个子类,这个子类的实例可以和其父类进行比较,因此就违背了LSP。这是因为,LSP原则是这样的,在任何你能使用父类的地方你都可以使用子类去替换它。在之前例子中,虽然cp的x,y坐标匹配那些在集合中的点,然而”coll.contains(cp)”仍然返回false,这看起来似乎违背得了LSP准则,因为你不能这里能使用Point的地方使用一个ColoredPointed。但是我们认为这种解释是错误的,因为LSP原则并没有要求子类和父类的行为一致,而仅要求其行为能一种方式满足父类的规范。
通过比较运行态的类来编写equals方法(译者注: canEqual的前一版本,使用.getClass()的那个版本)的问题并不是违背LSP准则的问题,但是它也没有为你指明一种创建派生类的实例能和父类实例进行对比的的方法。例如,我们使用这种运行态比较的技术在之前的”coll.contains(pAnon)”将会返回false,并且这并不是我们希望的。相反我们希望“coll.contains(cp)”返回false,因为通过在ColoredPoint中重载的equals,我基本上可以说,一个在坐标1,2上着色点和一个坐标1,2上的普通点并不是一回事。然而,在最后的例子中,我们能传递Point两种不同的子类实例到集合中contains方法,并且我们能得到两个不同的答案,并且这两个答案都正确。
–全文完–
Android GPS定位欺骗(模拟定位)的两种方式
前段时间发布的手游PokemonGo相信大家都有耳闻,而因为这个游戏在国内的坐标遭到了封锁,很多科学游戏方法也陆续涌现。好不热闹。
那其实,PokemonGo最初的版本,在大陆是可以通过简单的vpn+gps欺骗进行游戏的。
不过很快地,在新的版本更新中就封锁了这一方式。
而对Android系统使用GPS欺骗,应用场景也绝不只是这一个游戏而已。所以我今天来简单介绍一下可使用的几种方式。
控制噪声的方式有三种:防止噪声产生,阻断噪声传播和防止噪声进入耳朵
相对应的,
修改GPS定位结果的三种途径: 编译时修改NLP结果,运行时修改LocationManager结果,从应用获取到的结果修改。
1. 编译时修改NLP结果
难度系数:五颗星
建议:想都别想
大概思路:修改nlp部分源码,重编系统
2. 运行时修改LocationManager结果
这个分两类:
一类: 使用android自带的调试api,模拟gps provider的结果。
LocationManager.setTestProviderLocation(Provider, Location);
优点:简单,无需root
缺点:不稳定,特征明显,容易按特征嗅探到(有反作弊机制的游戏基本都能查出来),需要打开开发者的允许模拟位置选项
第二类: 使用xposed,传说中的android神器,用它对app_process进行注入。
有什么用呢,就是你可以放个钩子,英文名叫hook。这个钩子能知道你系统里的每个应用什么时候调用了哪个函数,还能修改对应的这个函数。
说到这就懂了吧。比如你猜测对应app会使用LocationManager.getLastKnownLocation的结果。然后你用xposed把内存里的这个函数返回值改成 纬度 N 39.832670° 东经 E116.460370°
,然后调用这个函数的程序看到的记过就是你修改之后的结果。
具体代码看这里吧(非本人repo,只是找了个简单易懂的demo)
FakeGPS demo
优点:稳定,难以被反查
缺点:需要root
3. 对想欺骗的app反编译,修改结果
该怎么做看标题就明白了。
步骤就是
1. 反编译
2. 找到所有使用了定位结果的位置
3. 修改结果
4. 重新打包
这个方式的优缺点也很明显。
优点: 无需root,稳定性强(前提是找准入口)
缺点: 技术水平要求高。根据应用复杂程度、混淆、安全策略等不同,难度差异较大。难易程度包括很多内容,包括混淆部分、入口寻找、签名验证等。 我也不熟啊,感兴趣的同学请自行深入学习吧。
————————
3类4种欺骗方式,各位看官收好。
以上。
微博自动发贴,简单却不容易被注意到的反爬方式
移动端登录后移步 http://m.weibo.cn/mblog 页面发贴,正常思路是:填写消息->其它选项->点击发送。
tv_msg.send_keys("msg")
btn_send.click()
仔细看下细节,会发现,发送按钮一开始是disable的,输入消息后才会变成enable。
所以按理说,代码确实没有问题。可是执行结果是最终停留在这个页面,而且send按钮并没有变成可用的橙色。
测试最后发现,msg输入后,send并不会立即改变状态,而是有一个很小的延迟时间。而因为机器的执行速度极快,导致在send状态改变前,已经执行了click动作。所以发送动作没有如期进行。
在其中加入一点delay解决问题。
tv_msg.send_keys("msg")
time.sleep(0.1)
btn_send.click()
利用 Python + Selenium 实现对页面的指定元素截图(可截长图元素)
对WebElement截图
WebDriver.Chrome自带的方法只能对当前窗口截屏,且不能指定特定元素。若是需要截取特定元素或是窗口超过了一屏,就只能另辟蹊径了。
WebDriver.PhantomJS自带的方法支持对整个网页截屏。
下面提供几种思路。
方式一
针对WebDriver.Chrome
通过WebDriver的js脚本注入功能,曲线救国。
1. 注入第三方html转canvas的js库(见下方推荐)
2. 获取元素html源码
3. 将html转换为canvas
4. 下载canvas
优点: 截取长图容易实现
缺点: 加载第三方库耗费时间,转换原理请参考这篇文章:
将 DOM 对象绘制到 canvas 中
方式二
针对WebDriver.Chrome
截取全图,自行裁剪、拼接
1. 获取元素位置、大小
2. 获取窗口大小
3. 截取包含元素的窗口
4. 进行相应的裁剪和拼接。
具体算法思路很清晰,但需要注意的细节较多。这里就不在赘述。示例代码请移步:
[Github]PythonSpiderLibs
优点: 不需太多js工作,python+少量js代码即可完成
缺点: 拼接等工作会被WebDriver的实现差异、图片加载速度等因素影响,需多加注意。 在保证截图质量的情况下,速度较慢
方式三
针对WebDriver.PhantomJS
由于接口实现的差异,PhantomJS相比于Chrome,可以截取到整个网页。所以获取指定元素的截图也就简单很多
- 截取网页全图
- 裁剪指定元素
driver = webdriver.Chrome()
driver.get('http://stackoverflow.com/')
driver.save_screenshot('screenshot.png')
left = element.location['x']
top = element.location['y']
right = element.location['x'] + element.size['width']
bottom = element.location['y'] + element.size['height']
im = Image.open('screenshot.png')
im = im.crop((left, top, right, bottom))
im.save('screenshot.png')
优点: 实现简单
缺点: 对于高度太高的页面会导致文件过大,处理会有问题,我测试的最大图片尺寸是12.8M。
解决图片加载不完整的问题
参考: 利用 Python + Selenium 自动化快速截图
我们先在首页上执行一段 JavaScript 脚本,将页面的滚动条拖到最下方,然后再拖回顶部,最后才截图。这样可以解决像上面那种按需加载图片的情况。
from selenium import webdriver
import time
def take_screenshot(url, save_fn="capture.png"):
browser = webdriver.Firefox() # Get local session of firefox
browser.set_window_size(1200, 900)
browser.get(url) # Load page
browser.execute_script("""
(function () {
var y = 0;
var step = 100;
window.scroll(0, 0);
function f() {
if (y < document.body.scrollHeight) {
y += step;
window.scroll(0, y);
setTimeout(f, 100);
} else {
window.scroll(0, 0);
document.title += "scroll-done";
}
}
setTimeout(f, 1000);
})();
""")
for i in xrange(30):
if "scroll-done" in browser.title:
break
time.sleep(10)
browser.save_screenshot(save_fn)
browser.close()
if __name__ == "__main__":
take_screenshot("http://codingpy.com")
不同wewbdriver对某些方法的实现不同
Chrome和PhantomJS 的接口差异
抓知乎时的坑,
1. Chrome用WebElement.text
可以正常得到值,用PhantomJS只能用 WebElement.get_attribute('innerHTML')
2. WebDriver.Chrome截图只能截当前屏幕区域。WebDriver.PhantomJS截图可以获取整个页面的长图。
其它还有一些坑等待发现
推荐
谈谈爬虫-模拟登录思路
最近在做的sideproject,需要网络上的文章数据。于是顺便学习了下爬虫技术,也算是有些心得体会。写下来分享给刚入坑的新人。
怎么理解模拟登录?
怎么理解模拟登录?
把这句话补全就是: 怎么(让机器)模拟(人在浏览器上的行为)登录(指定的网站)。
那么这个问题实际上问的是: 人通过浏览器登录网站时,浏览器为我们做了哪些事情。
那么我们需要做的只有:写一个脚本,让这个脚本模拟浏览器的行为,做我们希望它做的事情。
有兴趣参考:
当在浏览器地址栏输入一个URL后回车,将会发生的事情?
那么,
人类在登录时做了哪些事情呢,很简单:
1. 打开登录页面
2. 输入用户名密码,有时可能还有验证码,各种各样的验证码
3. 点击登录
4. 等待浏览器自动跳转
只要你稍微懂一点html语言,就应该能分析个八九不离十。
机器人怎么做呢:
两种方式:
方式一:
需要使用虚拟的浏览器引擎。
优点: 适合几乎所有的网站登录,可以人为输入验证码
缺点: 速度较慢
1. 请求登录页面的url,比如微博的(https://passport.weibo.cn/signin/login)
2. 分析html中的表单数据
2.1 找到输入用户名、密码的输入框
2.2 把输入框的text域替换成自己的用户名密码
3. 模拟点击提交按钮
方式二:
分析登录信息提交方式,一般就是表单
优点: 轻量,速度快
缺点: 局限性大,对技术要求高,对验证码机制需要做针对的破解
1. 使用浏览器的调试模式查看网页
2. 检查是否使用表单提交
3. 点击登录按钮,查看发送的请求数据。主要查看参数有无加密验证或其它隐藏信息。
4. 使用分析结果进行请求操作
对于一般用户,所有的非特殊性需求都可以使用方式一进行完成。
若非是为了学习,推荐方式一。
技术资料请参考:
Python爬虫学习系列教程(推荐)
[Python爬虫] Selenium爬取新浪微博移动端热点话题及评论 (下)
如何让脚本的行为看起来像人?
为什么要像人
因为很多服务器会使用一些反爬技术拒绝爬虫软件访问。
哪些东西让你看起来像人,哪些不像人
像人,其实可以分为两点。
一类是看请求数据,是否符合是浏览器发出的正常数据,比如header内容。
一类是看行为模式,发送请求对象的行为更像人类还是机器人,比如请求的频率。
不像人,和上面对应。
从请求数据上看,你没说明user-agent,我就可以认为你是非法侵入。你没有带着我之前给你的饼干(cookie)来,我也可以拒绝你。
从行为模式上,同一个ip访问的频率过高,短时间内流量异常,都可以作为非人类处理。
结合反爬技术
- 需要登录用cookie
- ip限制加代理
- 用user-agent告诉对方你是浏览器
- 服务器限制访问频率,加延迟
- ajax异步加载,使用js引擎或者人工分析
- redirect,最简单的方式虚拟内核+延迟
- 验证码,虚拟内核
如何找切入点?
什么是好的登录页面?
没有验证码,非ajax异步加载。
不一定局限于pc端网页,app端、移动端一般做的反爬策略比较少,可以从这里入手,寻找适合的站点。
理解自己要做什么,如何伪装成人类。仔细思考访问流程,针对性的有哪些反爬手段。把这些想通了,爬虫之路会好走很多。
转载请注明:未命名的博客
相关文章和资料
技术语言资料请自行google。
使用Github的Webhooks进行网站的自动化部署
使用mWeb做自己的博客,服务器没有直接使用github的gh-pages功能,而是部署到了自己的服务器上。
从此更新博客变成了三步走:1. 使用mWeb生成静态网页 2. push 到github 3. 登录服务器拉取最新内容。
昨天想到,能不能再简化一些步骤,让我的文章push到github后,让服务器自动拉取文章,部署新内容。说干就干,实施想法。
1. 目标
服务器自动拉取push到github上的新文章。
2. 想法
想法一: 定时检查置顶repo的提交,有更新,则启动部署流程。(主动查询方式)
想法二: github是否支持事件提醒或者第三方有无支持。(被动唤醒方式)(相当于消息推送)
3. 思考
主动查询,耗费cpu时间及流量,并且必然会和github产生同步间隔。
被动唤醒,不会消耗不必要的资源,若是支持必然是第一选项。
4. 查阅资料(可行性分析)
github支持Webhooks及大量的第三方服务,可以很好得对repo的push等操作做出反应。
Webhooks做了什么?
当github收到repo的操作行为时,会向指定的url发送一个带有描述操作内容的post请求。
5. 实现思路(总结)
对指定repo注册webhooks,指向我的服务器上的接口,服务器解析数据,若操作是push,则进行部署行为。
6. 实现
6.1 部署脚本:
deploy.sh
#!/bin/bash
LOG_FILE="/var/log/blog_deploy.log"
date >> "$LOG_FILE"
echo "Start deployment" >>"$LOG_FILE"
cd /Path/need/be/deployed/
echo "pulling source code..." >> "$LOG_FILE"
git checkout origin gh-pages
git pull origin gh-pages
echo "Finished." >>"$LOG_FILE"
echo >> $LOG_FILE
每当接收到带push的post请求时,执行上面的脚本。
6.2 处理post请求
注:以下nodejs内容摘自曾曦前辈博客-尘埃落定
然后我们就要写一个脚本在 http://dev.lovelucy.info/incoming 这里接受 POST 请求了。因为本人机器上跑的是 node,俺就找了个 nodejs 的中间件 github-webhook-handler 。如果你要部署的是 PHP 网站,那你应该找一个世界上最好的语言 PHP 的版本,或者自己写一个,只需要接收 $_POST 嘛,好简单的,不多废话啦。么么哒 ( • ̀ω•́ )
$ npm install -g github-webhook-handler
鉴于在天朝的服务器上 npm 拉 repo 比拉屎还难的状况,我们可以 选用 阿里的镜像,据说 10 分钟和官方同步一次。_(:3 」∠ )_
$ npm install -g cnpm --registry=http://r.cnpmjs.org
$ cnpm install -g github-webhook-handler
好了,万事俱备,下面是 NodeJS 的监听程序 deploy.js
var http = require('http')
var createHandler = require('github-webhook-handler')
var handler = createHandler({ path: '/incoming', secret: 'myHashSecret' })
// 上面的 secret 保持和 GitHub 后台设置的一致
function run_cmd(cmd, args, callback) {
var spawn = require('child_process').spawn;
var child = spawn(cmd, args);
var resp = "";
child.stdout.on('data', function(buffer) { resp += buffer.toString(); });
child.stdout.on('end', function() { callback (resp) });
}
http.createServer(function (req, res) {
handler(req, res, function (err) {
res.statusCode = 404
res.end('no such location')
})
}).listen(7777)
handler.on('error', function (err) {
console.error('Error:', err.message)
})
handler.on('push', function (event) {
console.log('Received a push event for %s to %s',
event.payload.repository.name,
event.payload.ref);
run_cmd('sh', ['./deploy.sh'], function(text){ console.log(text) });
})
/*
handler.on('issues', function (event) {
console.log('Received an issue event for % action=%s: #%d %s',
event.payload.repository.name,
event.payload.action,
event.payload.issue.number,
event.payload.issue.title)
})
*/
之后把服务器跑起来就可以了。
$ nodejs deploy.js
为了防止服务挂掉,我们有很多方式可以处理。我选择了用系统自带的nohup。
$ nohup nodejs deply.js &
曾曦前辈使用的是 NodeJs的forever,也可以使用python的supervisor。
曾曦前辈博客-尘埃落定有相关介绍。
6.3 配置Webhooks监听
将Payload URL指向自己服务器的接口
var handler = createHandler({ path: '/incoming', secret: 'myHashSecret' })
http.createServer(function (req, res) {
handler(req, res, function (err) {
res.statusCode = 404
res.end('no such location')
})
}).listen(7777)
这是deploy.js 的关键代码。
listen(7777),表明服务器监听的是7777端口
path:'/incoming',表示在 ip:7777/incoming 接收POST请求
secret: 'myHashSecret', 要求和上图的Secret字段一样,不然服务器会因为不匹配,拒绝接收到的请求。主要为了防止第三方向这个端口发送请求。
7. 最后梳理一下
6.3 那里知道什么时候有人提交文章了,然后告诉6.2 有人push
6.2 从6.3 得到消息,看下你的密码(secret)和我的一样不,如果一样,我就把这个消息告诉6.1
6.1 开始跑到github数据库拉取最新的数据,部署完成
澄清
有朋友告诉我,复制粘贴的部分比较多。即便加了转载说明,也不是很好。
在这里澄清一下:
网络上技术文章特点:多,杂,全
可用的经典实例: 少
自生产实例的成本: 费时
一篇全原创的优质文章需要:思考+原创实例+码字+重复前三项 。
而对于学习者而言,思想+实例+思路已经满足80%。
所以我认为,一篇能学到东西的技术文章,并不需要全原创。
清晰的思路+前人提供的经典实例+个人思考,传达到位即可。
前人都总结好了,你再发一遍,不是制造网络垃圾吗?
打造一个以思路清晰著称的博客,专注于技术文章整理、重成文是本博客存在的意义。我不是垃圾的生产者,我是大自然的清道夫。
欢迎关注个人微博斯科特安的时间 ,进行技术、非技术交流。
10分钟快速搭建无限制流量的"VPN"(shadowsocks协议)
Too Long No Read: 阅读标题和标重点部分就能了解全部内容。
1. 简介
良心声明: 有朋友说10分钟根本连文章都看不完,哪能建起个自己完全不熟悉的V P N 呢?
所以在这里必须解释一波:10分钟指的是开始动手到可以使用的时间。并不包括读这篇文章的时间,也不包括你在搭建服务过程中自我纠结的时间(比如,租多大的服务器?用什么密码?剁完手后又要饿多久的肚子才能给女票买下件内衣?之类。别问我为什么知道你一定是男的)最后,不包括运行出错,调试测试的时间。因为,按我说的做,你根本不可能失败!
接下来是简介:
说是无限制流量,其实骗你的啦(可爱)。说是VPN,其实不限于VPN。(本文搭建的也不是VPN,而是被称为Shadowsocks的协议。)
接下来解释:
原理: 租一个国外的服务器 -->这个服务器上搭建自己的VPN --> 通过这个VPN科学上网。
无限制流量:500G,1T,甚至更多。一月这么多,用的完吗?用不完不就相当于无限制。
VPN:既然有了自己墙外的服务器,就可以用它搭建任何自己想要的科学上网利器。本文章主要介绍当下最安全、最流行的ShadowSocks.
价格:很便宜!!很便宜!!很便宜!!
2. 工具篇
2.1 VPS 国外的服务器
VPS:Virtual Private Server 虚拟专用服务器
其实你就知道是个自己能用来搭建科学上网服务的主机就行了。
一般不了解的人,第一反应都是:卧槽,我指用个10G流量只能翻墙的VPN就几十块一个月。那租一个可以干很多事情、不限流量的服务器,岂不要几百几千?其实不用998、不用98,绝对用你想不到的价格,买到最不可思议的产品!
2019新年限时特惠!Vultr 虚拟服务器注册即得100美元奖励金!
点击注册得100美金
仅此一个月!
推荐:
1. 搬瓦工 (稳定推荐)
优点:便宜!!最低500G流量,年购19.99美刀,使用优惠码还可以再减1刀左右。相当于每月10元。这价格已经比大多数VPN便宜了。
支持支付宝交易!
支持30天内退款 一般一个工作日内就能回复,支付宝收到2~3天。
一键配置shadowsocks!如果使用搬瓦工,那后面的内容都不用看了,点下面的链接注册即可。
官方网址:https://bandwagonhost.com/
如果被墙也可使用: https://bwh8.net
两个都是官方地址
数据中心:美国西雅图、佛罗里达、洛杉矶、荷兰 套餐价格:64MB内存年3.99美元 / 96MB内存年4.99美元/128MB内存年5.99美元/512MB内存年9.99美元 简单介绍:IT7官方旗下的低价VPS主机产品,拥有速度较好的西岸亚利桑那州机房,最低年付仅需3.99美元,我们可以用来学习、工作项目演示,以及需要支持PPP/TUN搭建工具使用需求。拥有4个数据中心,而且可以自由切换IP,更换不同的IP,解决IP被封问题。
最新资料 可登录搬瓦工中文资料站进行查看
搬瓦工中文
2. Vultr (低价推荐,我自己目前在用)
2020年限时特惠!注册即得100美元奖励金!
点击注册得100美金
仅此一个月!
注册网址
优点
1. 注册赠送100美金。使用最低标准服务5刀/月,相当于可以免费使用20个月。
2. 服务稳定。至少我还没碰到过当机情况。
3. 第三点是缺点,想获得100美金,必须使用信用卡支付,并且扣除2.5美元的验证费。(以后会返还)
4. 现在已全面支持支付宝/微信付款,非常方便,也不需要绑定信用卡了。
点击注册并获取100美金
数据中心:日本、洛杉矶、英国、法国、德国、荷兰、澳大利亚等14个机房
套餐价格:KVM 768MB 15GB SSD 1TB月流量 $5/月
简单介绍:Vultr作为全球最大的游戏主机提供商背景之一,上线之后以高质的性价比、12个数据中心,以及新注册账户赠送5美金的账户使用金优惠促销,吸引广大的用户。作为我们用户,日本、洛杉矶等数据中心速度较好,如果有需要海外其他机房也可以在其12个数据中心中选择到适合自己的。
3. 其它
因为我指用过上面两个,所以其他的也不多介绍了。列个列表,大家可以自行google。
Linode: 很多人推荐。速度快。价格中等。
DigitalOcean: 很多人推荐。速度快,价格差不多。
为什么说价格便宜
除了明码标价的价格。其实本身已经和普通VPN价格差不多了。但是仍然,有一点。虽然流量并不是无限,但是带宽并没有限制。就是说,在流量还够用的前提下,和朋友一起使用,是几乎不影响访问速度的,价格又能再除以...,最后折算下来非常可观。当然,不能超越物理极限,3、5个人一起用,是保险又便利的方式。
价格上,贵就是好
对于同类物品,贵就是好。所以无论是我提到的,还是没提到的,虽然价格有差异,但是毕竟体现在服务好坏上。所以,如果你发现不同价格,买到了同样的配置,但是实际效果却有差距,这很正常。
2.2 Python Shadowsocks 搭建服务的工具和协议
Shadowsocks 属于socks5 代理,稳定性好,抗干扰能力强。
搭建服务 三步走
1 . 安装
在CentOS中运行下面两条命令就完成了shadowsocks的安装了:
yum install python-setuptools && easy_install pip
pip install shadowsocks
2 . 配置
完成之后创建一个配置文件 /etc/shadowsocks.json,写入以下内容:
{
"server":"0.0.0.0", #服务器IP地址
"server_port":8388, #服务监听端口
"local_port":1080, #本地连接端口
"password":"barfoo", #加密传输使用到的密码
"timeout":600, #连接超时时间
"method":"aes-256-cfb" #加密算法
}
3 . 启动、停止
运行下面的命令来启动和停止后台服务:
ssserver -c /etc/shadowsocks.json -d start
ssserver -c /etc/shadowsocks.json -d stop
然后你就可以使用上面的配置连接shadowsocks了。
- 客户端如何用?
各个平台使用的客户端都有差异,但是用到的信息就这些:
- 服务器IP: 不是上面的0.0.0.0,是你申请的VPS,会提供一个ip。打开网站,登录,找到它
- 端口(port): 8388
- 协议类型: aes-256-cfb 一般默认就这个,不用换。但还是要看一眼。
- 密码(password): barfoo
连接,欢呼。
Creating a Navigation Drawer
表现
Navigation Drawer
手指从屏幕左边右滑,打开的这个像 Menu 一样的的页面就被称为 NavigationDrawer。
谷歌在 MD 规范中详细说明了它的设计规范,在开始设计你的应用时请务必阅读作为参考。
Navigation Drawer
实现思路
分析一下,很容易看出来,基本是分为两层。上层的这个 Drawer 层(包含你看到的这个 View 和可能被忽略的覆盖下层的阴影),下层的包含了页面本身的内容层(ActionBar TabLayout 其它内容)。
使用方式
所以,使用也很符合思考模式:以 DrawerLayout 为根 View 包含两层子 View。
1. XML 文件
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<ListView android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="#111"/>
</android.support.v4.widget.DrawerLayout>
注意的点
* 内容层必须是DrawerLayout 的第一个子 View。因为 XML 以 View 的顺序决定 Z 的次序。
* drawer 层必须设定 android:layout_gravity 属性。用来支持 right-to-left 语言。注意用 "start" 代替 "left"。
* drawer 层的宽度小于320dp,这样展开时也能看到下方的内容。高度 match_parent。
2. 代码对内容进行配置
public class MainActivity extends Activity {
private String[] mPlanetTitles;
private DrawerLayout mDrawerLayout;
private ListView mDrawerList;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPlanetTitles = getResources().getStringArray(R.array.planets_array);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
// Set the adapter for the list view
mDrawerList.setAdapter(new ArrayAdapter<String>(this,
R.layout.drawer_list_item, mPlanetTitles));
// Set the list's click listener
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
...
}
}
3. 处理其它问题
3.1 处理点击事件导航
给 Drawer 的 ListView 设置 OnItemClickListener 就行。
示例代码
```
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
selectItem(position);
}
}
/** Swaps fragments in the main content view */
private void selectItem(int position) {
// Create a new fragment and specify the planet to show based on position
Fragment fragment = new PlanetFragment();
Bundle args = new Bundle();
args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
fragment.setArguments(args);
// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();
// Highlight the selected item, update the title, and close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mPlanetTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);
}
@Override
public void setTitle(CharSequence title) {
mTitle = title;
getActionBar().setTitle(mTitle);
}
```
监听 Drawer 展开、关闭事件
调用 DrawerLayout.setDrawerListener() 方法,传递一个 DrawerLayout.DrawerListener 接口的实现。
实现这个接口有两种方式。
1. 直接实现 DrawerListener.
2. 写一个继承 ActionBarDrawerToggle 的子类,这个类实现了这个接口,你可以在子类中重写 DrawerListener 接口函数。
示例代码
```
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private CharSequence mDrawerTitle;
private CharSequence mTitle;
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mTitle = mDrawerTitle = getTitle();
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
}
/* Called whenever we call invalidateOptionsMenu() */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// If the nav drawer is open, hide action items related to the content view
boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
return super.onPrepareOptionsMenu(menu);
}
}
```
通过 App Icon 打开、关闭 Drawer
通过使用 ActionBarDrawerToggle 类,可以控制这些行为。
* 点击控制 Drawer 行为
* 指定展开时的 icon
标准的导航 Icon 图标在 (Download the Action Bar Icon Pack)[http://developer.android.com/downloads/design/Android_Design_Icons_20130926.zip]
最后你需要在 Activity 的生命周期中使用 ActionBarDrawerToggle 的这些方法。
示例代码
```
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
...
public void onCreate(Bundle savedInstanceState) {
...
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerToggle = new ActionBarDrawerToggle(
this, /* host Activity */
mDrawerLayout, /* DrawerLayout object */
R.drawable.ic_drawer, /* nav drawer icon to replace 'Up' caret */
R.string.drawer_open, /* "open drawer" description */
R.string.drawer_close /* "close drawer" description */
) {
/** Called when a drawer has settled in a completely closed state. */
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
getActionBar().setTitle(mTitle);
}
/** Called when a drawer has settled in a completely open state. */
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
getActionBar().setTitle(mDrawerTitle);
}
};
// Set the drawer toggle as the DrawerListener
mDrawerLayout.setDrawerListener(mDrawerToggle);
getActionBar().setDisplayHomeAsUpEnabled(true);
getActionBar().setHomeButtonEnabled(true);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
mDrawerToggle.syncState();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDrawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Pass the event to ActionBarDrawerToggle, if it returns
// true, then it has handled the app icon touch event
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
...
}
```
风の诗(风之诗 Wind Song)
F4
音程
n度 大n度 小n度
和弦 和谐,不和谐
和弦 根音+1 3 5度,小三度+大三度 minor和弦, 大三度+小三度 大和弦, 小三度+小三度 减和弦
Copyright © 2015 Powered by MWeb, Theme used GitHub CSS.