tldr - 懒人的man page

其实我是十分不愿意在紧张开发时为了一个小工具翻看man page的,直到我从GitHub上找到这个工具。tldr即too long don’t read,提供简化版的man page,每页page只有非常简短的说明,十分适合懒人查询和情急时快速检索。当然,这个工具提供的page覆盖率还不够高,还有劳各位去发pull request。

https://github.com/tldr-pages/tldr

tl;dr

安装:

1
# npm install -g tldr

使用:

1
$ tldr aria2c

Done!

最后提一下,时间充裕时还是尽量去阅读完整版的man page!

用Xposed框架抓取微信朋友圈数据

因微信朋友圈为私有协议,从抓包上分析朋友圈数据几乎不可能,目前也尚未找到开源的抓取朋友圈的脚本。博主于是尝试通过使用安卓下的Xposed框架实现从微信安卓版上抓取朋友圈数据。
本文针对微信版本6.3.8。
GitHub仓库

主要思路

从UI获取文本信息是最为简单的方法,于是应该优先逆向UI代码部分。

逆向微信apk

首先解包微信apk,用dex2jar反编译classes.dex,然后用JD-GUI查看jar源码。
当然,能看到的源码都是经过高度混淆的。但是,继承自安卓重要组件(如Activity、Service等)的类名无法被混淆,于是还是能从中看到点东西。

  1. 首先定位到微信APP package。我们知道这个是com.tencent.mm
  2. com.tencent.mm中,我们找到一个ui包,有点意思。
  3. 展开com.tencent.mm.ui,发现多个未被混淆的类,其中发现MMBaseActivity直接继承自ActivityMMFragmentActivity继承自ActionBarActivityMMActivity继承自MMFragmentActivity,并且MMActivity是微信中大多数Activity的父类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class MMFragmentActivity
    extends ActionBarActivity
    implements SwipeBackLayout.a, b.a {
    ...
    }
    public abstract class MMActivity
    extends MMFragmentActivity {
    ...
    }
    public class MMBaseActivity
    extends Activity {
    ...
    }

现在需要找出朋友圈的Activity,为此要用Xposed hookMMActivity

创建一个Xposed模块

参考[TUTORIAL]Xposed module devlopment,创建一个Xposed项目。
简单Xposed模块的基本思想是:hook某个APP中的某个方法,从而达到读写数据的目的。
小编尝试hookcom.tencent.mm.ui.MMActivity.setContentView这个方法,并打印出这个Activity下的全部TextView内容。那么首先需要遍历这个Activity下的所有TextView,遍历ViewGroup的方法参考了SO的以下代码:

1
2
3
4
5
6
7
8
9
10
11
private void getAllTextViews(final View v) {
if (v instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) v;
for (int i = 0; i < vg.getChildCount(); i++) {
View child = vg.getChildAt(i);
getAllTextViews(child);
}
} else if (v instanceof TextView ) {
dealWithTextView((TextView)v); //dealWithTextView(TextView tv)方法:打印TextView中的显示文本
}
}

HookMMActivity.setContentView的关键代码如下:

1
2
3
findAndHookMethod("com.tencent.mm.ui.MMActivity", lpparam.classLoader, "setContentView", View.class, new XC_MethodHook() {
...
});

在findAndHookMethod方法中,第一个参数为完整类名,第三个参数为需要hook的方法名,其后若干个参数分别对应该方法的各形参类型。在这里,Activity.setContentView(View view)方法只有一个类型为View的形参,因此传入一个View.class
现在,期望的结果是运行时可以从Log中读取到每个Activity中的所有的TextView的显示内容。
但是,因为View中的数据并不一定在setContentView()时就加载完毕,因此小编的实验结果是,log中啥都没有。

意外的收获

当切换到朋友圈页面时,Xposed模块报了一个异常,异常源从com.tencent.mm.plugin.sns.ui.SnsTimeLineUI这个类捕捉到。从类名上看,这个很有可能是朋友圈首页的UI类。展开这个类,发现更多有趣的东西:
这个类下有个子类a(被混淆过的类名),该子类下有个名为gyOListView类的实例。我们知道,ListView是显示列表类的UI组件,有可能就是用来展示朋友圈的列表。

顺藤摸瓜

那么,我们先要获得一个SnsTimeLineUI.a.gyO的实例。但是在这之前,要先获得一个com.tencent.mm.plugin.sns.ui.SnsTimeLineUI.a的实例。继续搜索,发现com.tencent.mm.plugin.sns.ui.SnsTimeLineUI有一个名为gLZSnsTimeLineUI.a实例,那么我们先取得这个实例。

经过测试,com.tencent.mm.plugin.sns.ui.SnsTimeLineUI.a(boolean, boolean, String, boolean)这个方法在每次初始化微信界面的时候都会被调用。因此我们将hook这个方法,并从中取得gLZ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
findAndHookMethod("com.tencent.mm.plugin.sns.ui.SnsTimeLineUI", lpparam.classLoader, "a", boolean.class, boolean.class, String.class, boolean.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("Hooked. ");
Object currentObject = param.thisObject;
for (Field field : currentObject.getClass().getDeclaredFields()) { //遍历类成员
field.setAccessible(true);
Object value = field.get(currentObject);
if (field.getName().equals("gLZ")) {
XposedBridge.log("Child A found.");
childA = value;
//这里获得了gLZ
...
}
}
}
});

现在取得了SnsTimeLineUI.a的一个实例gLZ,需要取得这个类下的ListView类型的gyO属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void dealWithA() throws Throwable{
if (childA == null) {
return;
}
for (Field field : childA.getClass().getDeclaredFields()) { //遍历属性
field.setAccessible(true);
Object value = field.get(childA);
if (field.getName().equals("gyO")) { //取得了gyO
ViewGroup vg = (ListView)value;
for (int i = 0; i < vg.getChildCount(); i++) { //遍历这个ListView的每一个子View
...
View child = vg.getChildAt(i);
getAllTextViews(child); //这里调用上文的getAllTextViews()方法,每一个子View里的所有TextView的文本
...
}
}
}
}

现在已经可以将朋友圈页面中的全部文字信息打印出来了。我们需要根据TextView的子类名判断这些文字是朋友圈内容、好友昵称、点赞或评论等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void dealWithTextView(TextView v) {
String className = v.getClass().getName();
String text = ((TextView)v).getText().toString().trim().replaceAll("\n", " ");
if (!v.isShown())
return;
if (text.equals(""))
return;
if (className.equals("com.tencent.mm.plugin.sns.ui.AsyncTextView")) {
//好友昵称
...
}
else if (className.equals("com.tencent.mm.plugin.sns.ui.SnsTextView")) {
//朋友圈文字内容
...
}
else if (className.equals("com.tencent.mm.plugin.sns.ui.MaskTextView")) {
if (!text.contains(":")) {
//点赞
...
} else {
//评论
...
}
}
}

自此,我们已经从微信APP里取得了朋友圈数据。当然,这部分抓取代码需要定时执行。因为从ListView中抓到的数据只有当前显示在屏幕上的可见部分,为此需要每隔很短一段时间再次执行,让用户在下滑加载的过程中抓取更多数据。
剩下的就是数据分类处理和格式化输出到文件,受本文篇幅所限不再赘述,详细实现可参考作者GitHub上的源码。

在CentOS6上编译安装Python2.7

参考了友站的博文CentOS6上的Python2.7问题,正如所言,在CentOS6上安装Python2.7是非常头疼的问题。友站的这篇博文阐述了如何了从源安装Python2.7,本站则讲述从源码编译安装要注意的问题。
编译依赖参考How to install Python 2.7 and Python 3.3 on CentOS 6

依赖

十分重要: 编译Python2.7之前务必安装齐必须依赖。在configure过程中,若缺少依赖则不会报错,编译也可顺利通过,但编译出的Python将缺少几个必要模块,导致在运行ez_setup.py时出错。

1
2
# yum groupinstall "Development tools"
# yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel

编译和安装

1
2
3
4
5
$ wget http://python.org/ftp/python/2.7.6/Python-2.7.6.tar.xz
$ tar xf Python-2.7.6.tar.xz
$ cd Python-2.7.6
$ ./configure --prefix=/usr/local --enable-unicode=ucs4 --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
# make && make altinstall

这将会把Python2.7安装在/usr/local/bin/python2.7

将默认Python版本从2.6改为2.7

首先将/usr/bin/python这个软链接指向刚刚安装的Python2.7

1
2
# rm /usr/bin/python
# ln -s /usr/local/bin/python2.7 /usr/bin/python

重要: 进行这步操作后,yum会失效,运行即报错。这是因为/usr/bin/yum其实是个python2.6脚本,刚刚安装的python2.7缺少yum的相关依赖。因此需要改动/usr/bin/yum的解释器。

1
# vim /usr/bin/yum

将第一行

1
#!/usr/bin/python

改为:

1
#!/usr/bin/python2.6

现在运行yum --version应该不会再报错

安装pip

1
2
3
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py
# python ez_setup.py
# easy_install-2.7 pip

替换默认pip为pip2.7

1
2
3
4
# vim /usr/bin/pip2.6 #第一行改为#!/usr/bin/python2.6
$ which pip2.7 #应该返回/usr/local/bin/pip2.7
# rm /usr/bin/pip
# ln -s /usr/local/bin/pip2.7 /usr/bin/pip

PixivHack:记P站爬图脚本开发

由于7月学校考试,预习功课有点忙,最近又忙着做外包,一直都忘了维护博客,所以现在补上上个月本来应该写的技术博文。

这是啥

跟生活在2.5次元的老司机混久了便跟着入了ACG坑(看到本站首页LL大法时您应该意识到了,博主是个死宅(:з」∠) )。P站找图是每一个ACG爱好者的必备技能,然而像我这种刚入坑不久的,收藏的画师才几个,再者没有钱买Premium,不能按人气选图,每次找图都要手动一页一页翻(╯‵□′)╯︵┻━┻ 于是我想用Py写个自动爬图脚本。这个脚本可以按你输入的关键词搜索作品,并根据Rating(评分次数,以此来判断作品人气)的最小值来筛选并自动下载,也可以手动指定画师ID列表,也是按照设定最小Rating的方法下载每个画师的图。目前支持下载插画、漫画和大图。

你为什么还不用!

GitHub链接
使用方法详见README.md

实现过程

  • 要从P站搜图首先要登录。我原来的设想是通过抓包直接用Py模拟登录,但是不出所料,P站的登录API参数都是经过加密的(貌似是基于RSA的),在不知加密算法的情况下无法实现。(虽然在另一个开源项目中我已经能够分析登录部分的JS加密逻辑,但在那之前我还是懒得审查JS)
    于是目前的解决方案是要求用户在浏览器登录进P站一次,通过浏览器debugger获取Cookies中的PHPSESSID的值并输入到脚本中,脚本向P站发出的每次http请求都要带上该Cookie,目的是让P站服务器认为我们已经登录。
    下面贴一行每次请求带上Cookie的代码
    1
    search_result = self.__session.get('http://www.pixiv.net/search.php?word=' + urllib.quote(self.__keyword) + '&p=' + str(page), cookies={'PHPSESSID': self.__session_id})

更好的方法:在requests的session中通过headers.update()方法在Header中设置Cookie,该session的每次请求都能自动带上该header,这样就不需要每次都在请求中加上cookies参数

  • 从HTML源码中提取有用数据:虽然Python中可以使用HTMLParser更灵活地分析HTML,但是由于不想在这个小项目上浪费太多时间,我直接用正则从HTML中匹配。这里贴一行匹配作品搜索列表的代码:

    1
    result_list = re.findall(r'<a href="(/member_illust\.php\?mode=.*?&amp;illust_id=.*?)">', search_result.text)
  • 自动脚本的流程是:获取作品搜索结果页面,从每个搜索结果分别进入作品首页,判断Rating是否高于设定的最小值(若低于则跳过该作品),判断作品类型(插画/漫画/大图)并根据不同的流程进入作品二级页面并获取原图的URL

  • 绕过P站的防Bot机制:P站的原图不可直接下载。用户在浏览器中访问原图链接时,浏览器会自动加上Refer这个HTTP头,P站图片服务器会验证Refer是否合法。所以,脚本在访问原图链接并下载时,也需要在header中带上Refer。经过测试,Refer的值为作品首页或者作品二级页的URL。总之,在下载原图时带上当前页面(也就是HTML中能找到原图src的页面)的URL作为Refer就不会出问题。

    1
    download_result = self.__session.get(url, cookies={'PHPSESSID': self.__session_id}, headers={'Referer':referer})
  • 连接失败处理:经过测试,在长时间连续爬图时很有可能会有一两次requests请求超时(不排除是天朝某墙的TCP RST所致,也有可能是requests2.0在同域下长时间保持单TCP连接使P站服务器拒绝所致),因此,requests每次发出http请求时,都应该用try…except捕获超时异常并重试。

    1
    2
    3
    4
    5
    6
    try:
    page_result = self.__session.get(url, cookies={'PHPSESSID': self.__session_id}, headers={'Referer':referer})
    except Exception:
    print('Connection failure. Retrying...')
    self.__enter_manga_big_page(url, referer, directory)
    return
  • 统计画师总评分数:按关键词爬完一波图后,需要统计所爬的每个画师的ID、Rating等值,方便我们之后能收藏人气高的画师。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def __increment_author_ratings(self, author_id, increment, pixiv_id):
    for author in self.__author_ratings:
    if (author['author_id'] == author_id):
    if (pixiv_id in author['illust_id']):
    return
    author['total_ratings'] = author['total_ratings'] + increment
    author['illust_id'].append(pixiv_id)
    return
    self.__author_ratings.append({'author_id':author_id, 'total_ratings':increment, 'illust_id':[pixiv_id]})
  • 用户交互:用argparse实现CLI参数传入分析。在本脚本中,通过”-a”或”–authorlist”参数指定存储了画师ID列表的JSON文件。

    1
    2
    3
    parser = argparse.ArgumentParser()
    parser.add_argument('-a', '--authorlist', help='Crawl illustrations by author IDs. A JSON file containg the list of Pixiv member IDs is required.')
    args = parser.parse_args()

Using Gulp to Simplify Front end Development Workflow

What is Gulp

Gulp is a streaming build system which is usually used to simplify front-end development workflow, such as automatically minifying JavaScript or compiling LESS. In this tutorial, you’ll learn the basic usage of Gulp.

Preparation

To make sure you’ll have fun following my instructions, I assume you:

  • have node.js & npm installed
  • have basic knowledge of node.js scripting.

Installing Gulp

Let’s start with the installation of Gulp.
Run the following command in your shell terminal. The installation requires sudo or root previlege and you may be required to enter the password.

1
$ sudo npm install --global gulp

Project Setup

cd into the root directory of your project and run the following command, which will save the Gulp dependencies in your project directory.

1
$ npm install --save-dev gulp

Create gulpfile.js

At the root of your project, create a gulpfile.js containing the following code.

1
2
3
4
5
6
var gulp = require('gulp');

gulp.task('mytask', function() {
//All task code places here
console.log('Hello World!');
});

The code above defines a gulp task named “mytask” with the detailed commands defined in the callback function as the second parameter passed to the gulp.task() method. When running this task, console.log('Hello World!'); will be executed.

Testing

Now you should be able to run mytask using the following command:

1
$ gulp mytask

Assume the root directory of your project is ~/project, the output should be like:

1
2
3
4
5
➜  project  gulp mytask
[21:14:25] Using gulpfile ~/project/gulpfile.js
[21:14:25] Starting 'mytask'...
Hello World!
[21:14:25] Finished 'mytask' after 62 μs

You can always run specific tasks by executing gulp <task> <other_task>

Basic File Streaming

In this section we’ll use Gulp’s streaming system which is its primary function.
We will use Gulp.src(), Gulp.dest(), readable.pipe() to implement a basic JavaScript source file copying program using Gulp.
For detailed API doc of the methods above, please refer to Gulp API doc and Node.js:Stream.

  • Create a directory named js at the root of your project (Assume you created this directory: ~/project/js) and place some JavaScript files in it.
  • Add the following code at the end of gulpfile.js

    1
    2
    3
    4
    gulp.task('copyjs',function(){
    gulp.src('js/*.js')
    .pipe(gulp.dest('dest'));
    });
  • At the root of the project which is ~/project, run:

    1
    $ gulp copyjs
  • Check out ~/project/dest to which you’ll find all js files in ~/project/js are copied.

Using Gulp to Minify JS

Next we’ll use Gulp to do some amazing tasks which bring great convenience for front-end development.
Let’s start with JavaScript minifying.
To make Gulp powerful enouth to do this job, we must install some plugins of Gulp. Here we’ll use gulp-uglify.(For more amazing gulp plugins, check out Gulp Plugins)

  • Back to ~/project, run the following command to install gulp-uglify.

    1
    $ npm install --save-dev gulp-uglify
  • Replace gulpfile.js with the following code.

    1
    2
    3
    4
    5
    6
    7
    8
    var gulp = require('gulp');
    var uglify = require('gulp-uglify');

    gulp.task('minifyjs', function(){
    gulp.src('js/*.js') //Get the stream of the source file
    .pipe(uglify())//Pass the stream to the uglify module to minify all JS files.
    .pipe(gulp.dest('build'));//Pass the stream to the destination directory which is ~/project/build
    });
  • Exucute the task by running:

    1
    $ gulp minifyjs
  • Check out ~/project/build. All minified JavaScript source files are placed here!

Using Gulp.watch()

Sometimes we want the JS files to be automatically minified everytime we modify them and Gulp.watch() will do the trick.
Gulp.watch() allows us to implement a daemon to monitor file modifications and automatically execute specific tasks every time the modifications are made.

  • Add the following code at the end of gulpfile.js.

    1
    2
    3
    gulp.task('watchjs', function(){
    gulp.watch('js/*.js',['minifyjs']); //Watch all *.js files under ~/project/js directory and run task "minifyjs" when files are modified
    });
  • Execute the daemon task by running:

    1
    $ gulp watchjs
  • Now, JS files will be automatically minified every time you modify them.