写了一个推送图书到 Kindle 的 bot

最近因为好奇所以大致的看了一下 Telegram 的 bot,很早 就开始用 Telegram 但事实上,我大部分情况下就是将 Telegram 作为一个跨平台同步工具和备份工具来使用,直到最近因为 Ingress 的一些朋友和一个图书群我对 Telegram 才有了更多的使用,也在 Telegram 发现了每周会推送一本书的 Channel。

所以我想照着有一个叫做 to_kindle_bot 的号自己写了一个将图书发送到 kindle 邮箱的机器人,bot 要实现的功能很简单,就是将图书资源发送给 bot 之后将图书发送邮件推送到关联的 kindle 邮箱。将任务拆解开,主要的步骤也比较简单

  • 首先要了解的就是 Telegram bot 的 API
  • 再次存储用户的 kindle 邮箱
  • 再次就是将文件获取之后发送到对应的邮箱中

再做的过程中,我又增加了一些功能

  • 比如,在 bot 获取到图书之后会自动解析这本图书的 meta 信息和封面信息
  • 比如,kindle 有些格式不支持,所以要提前进行转码

所以一一解决这些问题即可。

Telegram Bot API

这一部分其实也没有什么要多说的,botFather 会创建好 bot,然后我用 Python 的 python-telegram-api。基本上也都是非常成熟的文档。

存储邮箱

因为早之前就已经做了一个图书相关的网站,所以理所当然的将用 telegram uid 和用户的 kindle 邮箱做了以下映射。

发送邮件

这部分其实之前也做过,所以直接使用 Flask mail 或者标准库里面的 smtp 去发就行了,邮件服务早之前 也做过调查,也是现成的用 mailgun 了。

获取图书信息

这一部分确实花了我一些精力,使用 python 的 ebooklib 库来解析了 epub 格式的电子书,也因为 epub 格式鱼龙混杂,所以修了几个 bug,但是 epub 格式的图书解析确实解决了,但是 mobi 格式我是没有找到任何方式来解析,不过幸好,调研的过程中发现了 Calibre 自带的 ebook-meta 命令行工具,在测试之后发现非常好用,支持的格式也非常多,几乎能够支持 99% 的情况,那这样一来获取图书 meta 和封面的工作也 OK。

图书格式转码

这部分一开始准备想要用 Amazon 提供的 kindlegen 命令行工具来实现的,但是发现他只能够将 其他的格式转为 mobi 格式,那这样局限就比较大了,虽然能够 cover 我现在的需求,但是如果以后有 mobi 格式转 epub 格式的需求,其实就有了问题,不够幸好还是 Calibre 这个强大的工具,提供了 ebook-convert 命令行工具,能够转化非常多的格式,所以理所当然的就采用了它。

至此就完成了 bot 90% 的工作,剩下就是组织一些文案和 bot 的零星的 bug 问题,所以可以试用下 @kindlepush_bot

bot 主要命令功能

bot 现在为止一共有 start help email settings 这几个功能,除开 settings 功能还未完成, email 命令用来设置 kindle 邮箱,start 和 help 命令用来显示帮助,settings 功能我设想可以让用户选择一个频率,bot 会自动在这个频率下发送一本书到 kindle 邮箱中。

另外如果大家喜欢电子书,我用 Flask 练手的电子书网站 https://book.einverne.info 也欢迎使用。

reference


2017-09-17 kindle , telegram , bot , python-telegram-api

每天学习一个命令:jstack 打印 Java 进程堆栈信息

Jstack 用于打印出给定的 java 进程 ID 或 core file 或远程调试服务的 Java 堆栈信息。

这里需要注意的是 Java 8 引入了 Java Mission Control,Java Flight Recorder,和 jcmd 等工具来帮助诊断 JVM 和 Java 应用相关的问题。推荐使用最新的工具以及 jcmd 来进行诊断。

Prints Java thread stack traces for a Java process, core file, or remote debug server. This command is experimental and unsupported.

jstack 命令能够:

  • Troubleshoot with jstack Utility
  • Force a Stack Dump
  • Stack Trace from a Core Dump
  • Mixed Stack

如果 java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。另外,jstack 工具还可以附属到正在运行的 java 程序中,看到当时运行的 java 程序的 java stack 和 native stack 的信息,如果运行的 java 程序呈现 hung 的状态,jstack 是非常有用的。

thread dump 就是将当前时刻正在运行的 JVM 的线程拷贝一份,可以用来分析程序执行情况。

用法

打印某个进程的堆栈信息

jstack [PID]

jstack -l [PID]
jstack -m [PID]
jstack -F [PID]

关于如何找到 PID,有很多方法,使用 jps -v 或者 ps -aux 或者 htop 等等方法都可以。

说明:

  • -l 选项会打印额外的信息,比如说锁信息, locks such as a list of owned java.util.concurrent ownable synchronizers,可以查看 AbstractOwnableSynchronizer
  • -F Force a Stack Dump

分析 jstack 输出

在执行 jstack -l [PID] > /tmp/output.txt 之后可以对 /tmp/output.txt 进行分析

jstack 输出开头是当前 dump 的时间和 JVM 基本信息(包括版本等):

2018-05-24 14:41:06
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.101-b13 mixed mode):

下面这个部分是 Safe Memory Reclamation(SMR) 安全的内存分配信息:

Threads class SMR info:
_java_thread_list=0x00007f3cd4005870, length=30, elements={
0x00007f3d14011800, 0x00007f3d142dd800, 0x00007f3d142e1800, 0x00007f3d142f4000,
0x00007f3d142f6000, 0x00007f3d142f8000, 0x00007f3d142fa000, 0x00007f3d14333800,
0x00007f3d14340000, 0x00007f3d14bc6800, 0x00007f3c900a1000, 0x00007f3c90255000,
0x00007f3c9025e800, 0x00007f3c90264000, 0x00007f3d14bdf800, 0x00007f3c64008800,
0x00007f3c6400b000, 0x00007f3d14c1e800, 0x00007f3c54025800, 0x00007f3c54027000,
0x00007f3c54042800, 0x00007f3c54044800, 0x00007f3c24005800, 0x00007f3c0c008800,
0x00007f3c0c00a000, 0x00007f3c0c00b800, 0x00007f3c48027000, 0x00007f3c48010000,
0x00007f3c48011000, 0x00007f3cd4004800
}

接下来就是程序的线程信息(非 VM 线程,非 GC 线程):

"main" #1 prio=5 os_prio=0 cpu=1071286.79ms elapsed=509136.64s tid=0x00007f3d14011800 nid=0xad5 runnable  [0x00007f3d1993a000]
   java.lang.Thread.State: RUNNABLE
		at org.eclipse.swt.internal.gtk.OS.Call(Native Method)
		at org.eclipse.swt.widgets.Display.sleep(Display.java:5570)
		at smartgit.Wx.d(SourceFile:305)
		at com.syntevo.smartgit.n.a(SourceFile:398)
		at com.syntevo.smartgit.n.a(SourceFile:247)

"Reference Handler" #2 daemon prio=10 os_prio=0 cpu=94.43ms elapsed=509136.51s tid=0x00007f3d142dd800 nid=0xadc waiting on condition  [0x00007f3cf10f4000]
   java.lang.Thread.State: RUNNABLE
		at java.lang.ref.Reference.waitForReferencePendingList(java.base@11.0.3/Native Method)
		at java.lang.ref.Reference.processPendingReferences(java.base@11.0.3/Reference.java:241)
		at java.lang.ref.Reference$ReferenceHandler.run(java.base@11.0.3/Reference.java:213)

   Locked ownable synchronizers:
		- None

线程信息又可以划分成几个部分。

Section Example 解释
线程名字 main 和 Reference Handler 可读的线程名字,这个名字可以通过 Thread 方法 setName 设定
线程 ID #1 每一个 Thread 对象的唯一 ID,这个 ID 是自动生成的,从 1 开始,通过 getId 方法获得
是否守护线程 daemon 这个标签用来标记线程是否是守护线程,如果是会有标记,如果不是这没有
优先级 prio=10 Java 线程的优先级,可以通过 setPriority 方法设置
OS 线程的优先级 os_prio  
CPU 时间 cpu=94.43ms 线程获得 CPU 的时间
elapsed elapsed=509136.51s 线程启动后经过的 wall clock time
Address tid Java 线程的地址,这个地址表示的是 JNI native Thread Object 的指针地址
OS 线程 ID nid The unique ID of the OS thread to which the Java Thread is mapped.
线程状态 wating on condition 线程当前状态 线程状态下面就是线程的堆栈信息
Locked Ownable Synchronizer    

线程的运行状态:

  • New: 线程对象创建,不可执行
  • Runnable: 调用 thread.start() 进入 runnable,获得 CPU 时间即可执行
  • Running: 执行
  • Waiting: thread.join()或调用锁对象 wait() 进入该状态,当前线程会保持该状态直到其他线程发送通知到该对象
  • Timed_Waiting:执行 Thread.sleep(long)、thread.join(long) 或 obj.wait(long) 等就会进该状态,与 Waiting 的区别在于 Timed_Waiting 的等待有时间限制;
  • Blocked: 等待锁,进入同步方法,同步代码块,如果没有获取到锁会进入该状态。该线程尝试进入一个被其他线程占用的 synchronized 块,当前线程直到锁被释放之前一直都是 blocked 状态
  • Dead:执行结束,或者抛出了未捕获的异常之后
  • Deadlock: 死锁
  • Waiting on condition:等待某个资源或条件发生来唤醒自己
  • Waiting on monitor entry:在等待获取锁
  • terminated 线程已经结束 run() 并且通知其他线程 joining

以上内容来自 Oracle

通过 jstack 信息可以分析线程死锁,或者系统瓶颈,但是这篇文章比较粗浅,只介绍了大概,等以后熟悉了补上。

reference


2017-09-14 jstack , java , debug , linux , thread-dump

Spring 中 HandlerMethodArgumentResolver 使用

HandlerMethodArgumentResolver 是一个接口:

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter var1);

    @Nullable
    Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}

HandlerMethodArgumentResolver 的接口定义如下:

  • supportsParameter() 用于判断是否支持对某种参数的解析
  • resolveArgument() 当判断支持后将请求中的参数值进行相应的转换

关于 HandlerMethodArgumentResolver 执行流程,大部分可以在类 InvocableHandlerMethod 中看到

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    // 获取 Controller 中函数的参数对象
    Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
    if (this.logger.isTraceEnabled()) {
        this.logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "' with arguments " + Arrays.toString(args));
    }

    Object returnValue = this.doInvoke(args);
    if (this.logger.isTraceEnabled()) {
        this.logger.trace("Method [" + ClassUtils.getQualifiedMethodName(this.getMethod(), this.getBeanType()) + "] returned [" + returnValue + "]");
    }

    return returnValue;
}

private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    // 获取执行的具体函数的参数
    MethodParameter[] parameters = this.getMethodParameters();
    Object[] args = new Object[parameters.length];

    for(int i = 0; i < parameters.length; ++i) {
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        args[i] = this.resolveProvidedArgument(parameter, providedArgs);
        if (args[i] == null) {
            // 首先判断是否有参数解析器支持参数 parameter,采用职责链的设计模式
            if (this.argumentResolvers.supportsParameter(parameter)) {
                try {
                    // 如果参数解析器支持解析参数 parameter,那么解析参数成 Controller 的函数需要的格式
                    args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                } catch (Exception var9) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug(this.getArgumentResolutionErrorMessage("Failed to resolve", i), var9);
                    }

                    throw var9;
                }
            } else if (args[i] == null) {
                throw new IllegalStateException("Could not resolve method parameter at index " + parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() + ": " + this.getArgumentResolutionErrorMessage("No suitable resolver for", i));
            }
        }
    }

    return args;
}

外延

解析请求头及参数 AbstractNamedValueMethodArgumentResolver

AbstractNamedValueMethodArgumentResolver 抽象类主要用来解析方法参数 (resolving method arguments from a named value),请求参数,请求头,path 变量都是 named value 的例子。他下面有很多的子类

AbstractCookieValueMethodArgumentResolver
ExpressionValueMethodArgumentResolver
MatrixVariableMethodArgumentResolver
PathVariableMethodArgumentResolver
RequestAttributeMethodArgumentResolver
RequestHeaderMethodArgumentResolver
RequestParamMethodArgumentResolver
ServletCookieValueMethodArgumentResolver
SessionAttributeMethodArgumentResolver

PathVariableMethodArgumentResolver

PathVariableMethodArgumentResolver 用来解析注解 @PathVariable 的参数。

@PathVariable 用来解析 URI 中的 Path 变量,Path 变量是必须的,并且没有默认值。

RequestParamMethodArgumentResolver

用来将请求参数解析到注解 @RequestParam 修饰的方法参数中。

比如请求 https://localhost:8080/hello?uid=1234

@RequestMapping(value="/hello",method=RequestMethod.POST)
@ResponseBody
public Map<String,Object> test(@RequestParam(value = "uid", required = true, defaultValue = "1") String uid){
    return m;
}

其中将请求的 uid 值解析到方法参数 uid 就是通过 RequestParamMethodArgumentResolver 来实现的。

解析请求体 AbstractMessageConverterMethodArgumentResolver

AbstractMessageConverterMethodArgumentResolver 是一个抽象类,主要用来将请求 body 中的内容解析到方法参数中,其子类:

  • AbstractMessageConverterMethodProcessor
  • HttpEntityMethodProcessor
  • RequestPartMethodArgumentResolver
  • RequestResponseBodyMethodProcessor

代码片段

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
    protected final List<HttpMessageConverter<?>> messageConverters;
    protected final List<MediaType> allSupportedMediaTypes;
}

AbstractMessageConverterMethodProcessor 的子类 RequestResponseBodyMethodProcessor:支持 @RequestBody@ResponseBody,使用举例:

@RequestMapping(value="/hello/test",method=RequestMethod.POST)
@ResponseBody
public Map<String,Object> test(@RequestBody Map<String,Object> m){
    return m;
}

reference

  • Spring doc

2017-09-10 spring , resolver , spring-boot

使用 antigen 来管理 zsh 插件

antigen 是 zsh 的插件管理工具,在他 GitHub 主页上的一句话非常形象的解释了他的功能。

Antigen is to zsh, what Vundle is to vim.

安装

curl -L git.io/antigen > antigen.zsh

或者

apt-get install zsh-antigen

或者直接 git clone 该项目,然后指定 antigen.zsh 的位置。

配置

如果使用过 Vim 的 Vundle 对 antigen 的配置应该不陌生。

    source /path-to-antigen-clone/antigen.zsh

    # Load the oh-my-zsh's library.
    antigen use oh-my-zsh

    # Bundles from the default repo (robbyrussell's oh-my-zsh).
    antigen bundle git
    antigen bundle heroku
    antigen bundle pip
    antigen bundle lein
    antigen bundle command-not-found

    # Syntax highlighting bundle.
    antigen bundle zsh-users/zsh-syntax-highlighting

    # Load the theme.
    antigen theme robbyrussell

    # Tell antigen that you're done.
    antigen apply

使配置生效 source ~/.zshrc

可以从这个页面 查看更多的插件。

更加详细的配置可以参考我的配置

直接在终端中使用 antigen

在安装 antigen 之后可以直接在命令行输入 antigen version 来查看版本。或者使用其他命令来直接安装插件,更新插件等等。

➜ antigen version
apply       -- Load all bundle completions
bundle      -- Install and load the given plugin
bundles     -- Bulk define bundles
cache-gen   -- Generate cache
cleanup     -- Clean up the clones of repos which are not used by any bundles currently lo
help        -- Show this message
init        -- Load Antigen configuration from file
list        -- List out the currently loaded bundles
purge       -- Remove a cloned bundle from filesystem
reset       -- Clears cache
restore     -- Restore the bundles state as specified in the snapshot
revert      -- Revert the state of all bundles to how they were before the last antigen up
selfupdate  -- Update antigen itself
snapshot    -- Create a snapshot of all the active clones
theme       -- Switch the prompt theme
update      -- Update all bundles
use         -- Load any (supported) zsh pre-packaged framework
version     -- Display Antigen version

reference


2017-09-07 antigen , zsh , bash , linux , vim , tmux

爬虫相关技术整理

部分内容从 Python 3 网络爬虫开发实战 中整理。

Python 模块

主要依赖 Python 模块

数据库

  • MySQL
  • Redis

抓包

工具依赖

  • Selenium 自动化测试框架
  • Appium 移动端自动化测试框架

爬虫框架


2017-09-05 crawler , spider , python , mitm , linux

使用 Shell 命令来对 Unix 时间戳和日期进行转换 date 命令

在程序中经常要使用到 Unix timestamp 和日期的转换,通常情况下都是 Google 一个时间戳转换的网页在进行转换,其实 Linux 命令中就有能够快速实现转换的命令。主要都是集中在 date 这个命令。date 命令主要用于显示或设定系统时间和日期。

修改系统的时区

Linux 用来修正系统的时区

sudo dpkg-reconfigure tzdata

选择 Asia > Shanghai

date 常用命令

获取当前的 unix timestamp

date +%s    # 返回 10 位时间戳,%s 表示从 1970-01-01 0 点 (epoch 开始的秒数)
date +%s%3N # 返回 13 位时间戳,毫秒
date +%s%N  # 返回 10 + 9 位纳秒

将时间戳转换成日期

$ date +%s
1504516338
$ date -d @1504516338
Mon Sep  4 17:12:18 CST 2017

将 string 日期转成日期

使用 -d 参数可以用来将输入 String 转成特定格式日期,如果不指定具体时间,date 会使用 00:00:00

$ date -d "06/04/1989"
Sun Jun  4 00:00:00 CDT 1989 1559192456
$ date -d "04 June 1989"
Sun Jun  4 00:00:00 CDT 1989
$ date -d "June 04 1989"
Sun Jun  4 00:00:00 CDT 1989
$ date -d "June 04 1989 12:01:01"
Sun Jun  4 12:01:01 CDT 1989

-d 选项也有一些其他很强大的功能,比如

$ date -d '5 minutes ago' # 5 分钟前的时间
Mon Sep  4 17:22:58 CST 2017
$ date -d '100 days'      # 100 天以后的日期
Wed Dec 13 17:29:14 CST 2017
$ date -d '-100 days'     # 100 天以前的日子
Sat May 27 17:30:01 CST 2017
$ date -d '100 days ago'  # 同上
Sat May 27 17:31:10 CST 2017
$ date -d 'next monday'
Mon Sep 11 00:00:00 CST 2017

或者 -d 选项还可以有这样的语法

date -d@1559192456

格式化参数

可以使用 + 来输出不同格式

date +%<format options>

比如

$ date '+%Y-%m-%d %H:%M:%S'
2017-09-04 17:38:46
Format options Purpose of Option Output
date +%a 缩略方式显示星期 (like Mon, Tue, Wed) Thu
date +%A 全称显示星期 (like Monday, Tuesday) Thursday
date +%b Displays Month name in short (like Jan, Feb, Mar ) Feb
date +%B Displays Month name in full short (like January, February) February
date +%d Displays Day of month (e.g., 01) 07
date +%D Displays Current Date; shown in MM/DD/YY 02/07/13
date +%F Displays Date; shown in YYYY-MM-DD 2013-02-07
date +%H Displays hour in (00..23) format 23
date +%I Displays hour (01..12) format 11
date +%j Displays day of year (001..366) 038
date +%m Displays month (01..12) 02
date +%M Displays minute (00..59) 44
date +%S Displays second (00..60) 17
date +%N Displays nanoseconds (000000000..999999999) 573587606
date +%T Displays time; shown as HH:MM:SS Note: Hours in 24 Format 23:44:17
date +%u Displays day of week (1..7); 1 is Monday 4
date +%U Displays week number of year, with Sunday as first day of week (00..53) 05
date +%Y Displays full year i.e. YYYY 2013
date +%Z alphabetic time zone abbreviation (e.g., EDT) IS

reference


2017-09-04 shell , linux

Mastering the Vim

我已经用了很长一段时间 Vim 了,但是 Vim 最可贵之处便在于你永远达不到 Vim 的天花板,在使用的过程中我永远会发现操作 Vim 的其他便捷方法。最近看了一个关于 Vim 的讲座 ,革新我对 Vim 命令的认识。可以说掌握这样的一个世界观可以对 Vim 的操作上到另外一个层次。下面就总结一下这个视频中的精髓内容。

Text Objects and motions

@ChrisToomey 定义了一种 Vim Language,Vim 的语法由数词 + 动词 + 名词 组成,比如:

d 删除
w 单词
将两个字母组合起来就是 删除单词

这个经常使用的命令非常容易记住。如果想要删除三个单词,则是 3dw,所以可以总结出

{number}{command}{text-object}

这样的形式

  • number 是数量
  • command 是动作
  • text-object 是对象

重复和撤销,相信使用过一段时间 Vim 的人应该会知道 . 表示重复上一次命令, u 表示撤销上一次操作。而重复和撤销是针对命令而不是针对输入的,因此每使用一次 . 或者 u 都是命令级别。因此这就给予了 . 操作非常强大的功能。

Verbs: 常用的动作举例

d Delete
c Change  delete and enter insert mode
y yank
>  intend 缩进
v 选择

Nouns: 常见的动作 Motion

w 移动到下一个 word 开始
e 移动到下一个 word 的结尾
b 移动到上一个 word 的开始 back
2j   向下移动 2 lines

Vim 中定义了很多移动操作

基于内容 Nouns: Text Objects

w => words 表示移动一个单词
s => sentences 移动一个句子
p => paragraphs 向下移动一个段落
t => tags  (html xml)

动作 motions

a => all
i => in
t => 'till
f => find forward
F => find backward

iw => inner word
it => inner tag
i" => inner quotes
ip => inner paragraph
as => a sentence

命令 commands

d => delete(also cut)
c => change(delete, then into insert mode)
y => yank (copy)
v => visual select

组合举例

diw  delete in word,即使光标在 word 中也能够快速删除 word
yi)  yank all text inside the parentheses,光标在 `()` 中复制括号中的所有内容

上面的 Text Object

{command}{text object or motion}

在单词中间,diw 删除光标下的单词,dit 删除光标下 tag 中的内容

Nouns: Parameterized Text Objects

f,F => find the next character
t,T => find till
/,?   => search

比如有一行文本

the program print ("Hello, World!")

如果想要删除 Hello 前面的内容,可以在行首使用 dtH , 解读这个命令可以理解为 d => delete unti H 从这里删除直到遇到 H。典型的 verb + noun 组合。

记住动作加移动,能够很快的记忆很多命令。

比如使用 https://github.com/tpope/vim-surround 这个插件,可以轻松的实现,使用命令 cs"' 来将

"hello world"

变成

'hello world'

命令的语义解释就是 change surroundding double quote to single quote 将包围的双引号变成单引号

使用 cs'<p> 将单引号变成 <p>

<p>hello world</p>

使用 cst" 将 surrounding 变成双引号

"hello world"

或者可以使用 ds" 来将 surrounding 双引号删除 delete surrounding “

hello world

DOT command

Vim 中的 “.” 命令,以命令为单位,重复上一个命令。

Sublime , IntelliJ IDEA 中经常被人提及的 multiple cursor 的功能,能够在编辑器中提供多个光标一起编辑,其实 Vim 不需要多光标就能够做到,结合强大的 .n . 可以快速的编辑大量重复的内容。

比如在 Vim 中的 workflow 就是

  • 使用 /pattern 来所有需要编辑的内容
  • 使用可编辑的 edit,比如 cw 当前单词 ESC 退出,一个完成的动作
  • 使用 n 来找到下一个匹配的地点
  • 使用 . 来重复编辑操作,可以直接将单词替换为上一个动作编辑
  • 然后重复上面两个步骤,整个文件即可替换完成

macro

Vim 允许记录一个宏来完成一组命令

qa                  # 将命令记录到寄存器 a 中
q                   # 再次 q 结束记录
@a                  # 使用寄存器
@@                  # 使用上一次寄存器

refernence


2017-09-03 vim , linux , editor

Raspberry pi 自动挂载 NTFS USB 设备

一些相关的命令

sudo fdisk -l    # 列出磁盘分区表

结果是这样的:

Disk /dev/ram0: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram1: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram2: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram3: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram4: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram5: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram6: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram7: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram8: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram9: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram10: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram11: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram12: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram13: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram14: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/ram15: 4 MiB, 4194304 bytes, 8192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes


Disk /dev/mmcblk0: 7.4 GiB, 7948206080 bytes, 15523840 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x1fdbda7f

Device         Boot Start      End  Sectors  Size Id Type
/dev/mmcblk0p1       8192    93813    85622 41.8M  c W95 FAT32 (LBA)
/dev/mmcblk0p2      94208 15523839 15429632  7.4G 83 Linux


Disk /dev/sda: 1.4 TiB, 1500301909504 bytes, 2930277167 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x4c63688c

Device     Boot Start        End    Sectors  Size Id Type
/dev/sda1        2048 2930272255 2930270208  1.4T  7 HPFS/NTFS/exFAT

在最后可以看到一块磁盘 /dev/sda1

然后可以使用如下的方式手动挂载。

手动挂载

为了让 Linux 能够读取 NTFS 格式的硬盘,需要安装 ntfs-3g 。然后可以手动挂载。

sudo apt-get install ntfs-3g
sudo mkdir -p /media/sda1
sudo mount -t ntfs-3g /dev/sda1 /media/sda1

挂载完成后可以查看

sudo df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/root       7.3G  2.2G  4.8G  31% /
devtmpfs        460M     0  460M   0% /dev
tmpfs           464M     0  464M   0% /dev/shm
tmpfs           464M   13M  452M   3% /run
tmpfs           5.0M  4.0K  5.0M   1% /run/lock
tmpfs           464M     0  464M   0% /sys/fs/cgroup
/dev/mmcblk0p1   42M   21M   21M  51% /boot
tmpfs            93M     0   93M   0% /run/user/1000
/dev/sda1       1.4T  1.1T  363G  75% /media/sda1

通过编辑 fstab 来让系统自动挂载

sudo vim /etc/fstab

插入

/dev/sda1 /mnt/hdd ext4 defaults 0 0

从而实现 USB 设备的自动挂载

sudo vim /etc/udev/rules.d/10-usbstorage.rules

KERNEL!="sd*", GOTO="media_by_label_auto_mount_end"
SUBSYSTEM!="block",GOTO="media_by_label_auto_mount_end"
IMPORT{program}="/sbin/blkid -o udev -p %N"
ENV{ID_FS_TYPE}=="", GOTO="media_by_label_auto_mount_end"
ENV{ID_FS_LABEL}!="", ENV{dir_name}="%E{ID_FS_LABEL}"
ENV{ID_FS_LABEL}=="", ENV{dir_name}="Untitled-%k"
ACTION=="add", ENV{mount_options}="relatime,sync"
ACTION=="add", ENV{ID_FS_TYPE}=="vfat", ENV{mount_options}="iocharset=utf8,umask=000"
ACTION=="add", ENV{ID_FS_TYPE}=="ntfs", ENV{mount_options}="iocharset=utf8,umask=000"
ACTION=="add", RUN+="/bin/mkdir -p /media/%E{dir_name}", RUN+="/bin/mount -o $env{mount_options} /dev/%k /media/%E{dir_name}"
ACTION=="remove", ENV{dir_name}!="", RUN+="/bin/umount -l /media/%E{dir_name}", RUN+="/bin/rmdir /media/%E{dir_name}"
LABEL="media_by_label_auto_mount_end"

如果要卸载文件系统,比如将挂载的 /media/sda1 卸载

umount /media/sda1

2017-09-02 linux , raspberrypi , mount , ntfs , usb

Spring MVC 应用处理 CORS

什么是跨域或者说什么是CORS(Cross-origin resource sharing),中文叫”跨域资源共享”。在了解 CORS 之前首先要知道“同源策略”,出于安全考虑,浏览器会限制Ajax中发起的跨站请求。比如,使用 XMLHttpRequest 对象发起 HTTP 请求就必须遵守同源策略(same-origin policy),”同源策略“是浏览器安全的基石。具体而言,Web 应用程序能且只能使用 XMLHttpRequest 对象向其加载的源域名发起 HTTP 请求,而不能向任何其它域名发起请求。阮一峰写的一篇关于 CORS 的文章 介绍得非常详细,这里主要记录一下重点以及 Spring MVC 中如何处理 CORS。

CORS 做到了不破坏即有规则,只要服务端实现了 CORS 接口,就可以跨源通信。

简单请求 VS 非简单请求处理

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

需要同时满足以下两大条件,才属于简单请求。

  • 请求方法仅仅为以下三种方法之一:

    HEAD、GET、POST

  • HTTP的头信息不超出以下几种字段:

    Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

简单请求处理

Response Header 选项
Access-Control-Allow-Origin 必须,值要么是请求的 Origin,要么是 * ,表示接受所有域名请求
Access-Control-Allow-Credentials 该字段可选。它的值是一个布尔值,表示是否允许客户端发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
Access-Control-Expose-Headers 该字段可选。扩展客户端能够访问的字段。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。

简单请求的处理过程可以参考下图:

简单请求流程

对于简单请求,CORS 的策略是请求时,在头信息中添加一个 Origin 字段,服务器收到请求后,根据该字段判断是否允许该请求。

  • 如果允许,则在 HTTP 头信息中添加 Access-Control-Allow-Origin 字段,并返回正确的结果
  • 如果不允许,则不在头信息中添加 Access-Control-Allow-Origin 字段。

浏览器先于用户得到返回结果,根据有无 Access-Control-Allow-Origin 字段来决定是否拦截该返回结果。

script 或者 image 标签触发的 GET 请求不包含 Origin 头,所以不受到 CORS 的限制,依旧可用。如果是 Ajax 请求,HTTP 头信息中会包含 Origin 字段,由于服务器没有做任何配置,所以返回结果不会包含 Access-Control-Allow-Origin,因此返回结果会被浏览器拦截,接口依旧不可以被 Ajax 跨源访问。

非简单请求

而对于真正实现中的请求,可能会使用 Content-Type:application/json,也有可能有自定义 Header,所以了解非简单请求的处理也非常必要。

对于 Content-Typeapplication/json 的特殊请求,需要服务端特殊对待的请求,在正式通信前会增加一次“预检”请求(preflight)。浏览器会先询问服务器,当前网页所在的域名是否在服务器的许可名单,以及可以使用哪些HTTP动词和头信息,得到服务端回复才会发出正式的请求,否则报错。

CORS 请求相关 Header

Request Header value
Access-Control-Request-Method 真实请求使用的 HTTP 方法
Access-Control-Request-Headers 真实请求包含的自定义 Header

在服务端收到客户端发出的预检请求后,校验 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers,通过校验后在返回中加入如下的header:

Response Header value
Access-Control-Allow-Methods 必须,值为逗号分割的字符串,表明服务器支持的所有跨域请求方法,返回的是所有支持的方法,为了避免多次“预检”请求。
Access-Control-Allow-Headers 如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
Access-Control-Allow-Credentials 与简单请求含义相同
Access-Control-Max-Age 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

非简单请求流程

Spring 处理跨域

这里主要针对 Spring 3.x 来处理, 在 Spring 4.2 之后官方引入了 @CrossOrigin 注解,处理 CORS 变的非常方便。所以接下来就记录下 3.x 中的处理方法。

更新 web.xml

更新 web.xml 让 Spring 开启 OPTIONS 处理.

<servlet>    
   <servlet-name>application</servlet-name>    
   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
   <init-param>  
		<param-name>dispatchOptionsRequest</param-name>  
		<param-value>true</param-value>  
   </init-param>    
   <load-on-startup>1</load-on-startup>    
</servlet>    

添加Header

使用 Interceptor

public class CorsInterceptor extends HandlerInterceptorAdapter {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		addOriginHeader(request, response);
		if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
			response.setStatus(200);
			return false;
		}	
		return true;
	}

	private void addOriginHeader(HttpServletRequest request, HttpServletResponse response) {
		String origin = request.getHeader("Origin");
		response.addHeader("Access-Control-Allow-Origin", origin);
		response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type");
		response.addHeader("Access-Control-Allow-Credentials", "true");         // 可选,是否允许Cookie
		response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
		response.addHeader("Access-Control-Max-Age", "1728000");
	}
}

在 XML 中配置 Interceptor

然后在 Controller 中

@RequestMapping(value = "/test/hello", method = {RequestMethod.GET, RequestMethod.OPTIONS})

然后OK


2017-09-01 Spring , CORS , JS , Web , HTTP , 跨域,

爬虫相关材料整理

这篇文章用来收集整理爬虫相关的资料。

相关技术

如果只想单纯的自己开发,可以使用 Python + Celery + Redis/MySQL 基本能满足 80% 的需求。

如果想要选用框架 Scrapy,pyspider,等等都是非常不错的选择,我甚至在 GitHub 上看到过 Java 的分布式爬虫。

书籍

Python 3 网络爬虫开发实战

这本书在网上有部分 gitbook,链接在这里

网上公开的部分都是无关痛痒的部分,不过提及的工具倒是可以参考一下。大部分我之前的文章也都有提及


2017-08-29 collection , spider , crawler , python , redis , mysql

电子书

最近文章

  • 使用 assh 来管理 SSH config 前两天一直在思考如何管理我的 SSH config 配置,最后的解决办法就是通过 git 版本管理起来。但这两天由冒出一个新的问题,那就是经常在国内直连 aws 或者 oracle 的机器时 ssh 连不上,但是通过国内的 VPS 中转就非常快,那这就意味着,我每一次连接国外的机器时必须先登录腾讯云的机器,然后在从腾讯云的机器上连过去,有些麻烦,但那天在 Twitter 上看到有人分享了一个 SSH 管理的命令行工具 assh,大致的看了一下使用简介,通过配置就可以完美的解决这个问题。
  • 备份和恢复从 Chrome Webstore 中下架的 Google Chrome Extension 这两天重装系统同步 Chrome 的数据才发现,我一直使用的 Dream Afar New Tab 这个我用了很久的扩展从 Chrome Webstore 消失了,不清楚是 Google 主动下架,还是作者很久没有更新被 Webstore 下了还是为什么。但这个扩展经过了很多的 Chrome 版本依然运行良好至今为止都能每天给我提供世界不同地方的美景。
  • Docker 网络与容器互联 简单整理一下 Docker 中 network 子命令,以及 docker 中相关 network 方面的内容。
  • MacBook Pro 初始设置记录 这里就简单的记录一下我从 Linux Mint 迁移到 MacOS 根据我的个人需求来初始化新的 MacBook Pro 的一些设置,和一些基本的感想。下面的内容会按照我自身的需求出发,我会列举我想要的功能然后在此基础上我需要借助哪些工具来实现。在切换到 MacBook Pro 之前,我使用了大约 6 年多的 Linux Mint,我已经有一套我自己的 Workflow,在切换到 Mac OS 之前我就在想哪一些的事情我是必须有 Mac 的软硬件才能做到,并且很提高某一方面的效率的,我列了一些
  • 多设备间同步 ssh 配置及密钥 ssh 客户端会在用户目录 ~/.ssh/ 目录下存放配置信息 (~/.ssh/config) 和公钥和私钥,如果有多个设备不同设备间的同步和管理就会成为一个比较头疼的问题。我在 Reddit 上抛出这个问题 后,我本来想的是通过 git 版本控制来进行管理,但有人说因为公钥和私钥都是二进制的文件,其实没有必要使用 git,任何一个同步工具就能够解决。