开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Scala 与设计模式(五):Adapter 适配器模式

发表于 2017-11-20

本文由 Prefert 发表在 ScalaCool 团队博客。

不管你是不是果粉,肯定对 iphone X 都有所耳闻。最近的「掉漆门」和「人脸识别被破解」更是将其推到了风口浪尖上。


但是对于我而言,最难以忍受的还是耳机接口被取消这一改变(自 Iphone 7 开始),你可以想象这样一幅画面:当你开开心心地和小伙伴开着语音吃(song)着(kuai)鸡(di)或者是多人一起上(song)分时——你的电量见底,为了不影响队友(shou)的游戏体验,肯定得充电玩下去。

这时你得面对一个难题:只有一个孔,插耳机还是插电源!?(在没有蓝牙耳机的前提下)

(侵删)

由于生活经常会欺骗我们,以及各种环境因素,所以不是每个人都选择蓝牙耳机(贫穷使我理智)。

是否存在别的解决方法呢?还好有转接线这样的好东西

(侵删)

在编程中,我们也会遇上类似的问题:

  1. 当你想使用一个已经存在的类,而它的接口不符合你的需求;
  2. 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作;
  3. …

本文会通过 Adapter Pattern 来探究如何解决这类问题。

本篇文章结构如下:

  • adapter pattern 的概念
  • 实际问题分解
  • Java 实例
  • Scala 实例
  • 总结

概念

适配器模式(Adapter Pattern)有时候也称包装样式或者包装(Wrapper)。定义如下:

将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中。

它解决了什么问题

适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用,在 Spring 等框架、驱动程序设计(如 JDBC 中的数据库驱动程序)中也使用了适配器模式。

Java 版本

小 A 是个苹果控 + 耳机控,之前买了一款很贵的耳机,对其爱不释手。我们都知道一般耳机接口都是 3.5mm 的。

1
2
3
4
5
6
7
8
9
10
11
复制代码public interface PhoneJackInterface {
// 传统的播放音频
void audioTraditionally();
}

public class PhoneJackConnector implements PhoneJackInterface {
@Override
public void audioTraditionally() {
System.out.println("通过 PhoneJack 播放声音");
}
}

iphone 7 之前的 iphone 支持 3.5mm 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class Iphone {
private PhoneJackInterface phoneJack;

public Iphone(PhoneJackInterface phoneJack) {
this.phoneJack = phoneJack;
}

// Iphone 具备播放声音的功能
public void play() {
// 通过 3.5mm 接口耳机播放
phoneJack.audioTraditionally();
}
}

这样的情况下,小 A 还可以愉快地听歌:

1
2
3
4
5
复制代码// test
PhoneJackInterface phoneJack = new PhoneJackConnector();
Iphone iphone6 = new Iphone(phoneJack);
iphone6.play();
// 控制台输出 “通过 PhoneJack 播放声音”

在 iphone 7 问世后,问题出现了:小 A 发现其不支持 3.5mm 接口 —— 将有线耳机的插口改为了 lightning。

1
2
3
4
5
6
7
8
9
10
复制代码public interface LightningInterface {
void audioWithLightning();
}

public class LightningConnector implements LightningInterface {
@Override
public void audioWithLightning() {
System.out.println("通过 Lightning 播放声音");
}
}

一边是耳机,一边是手机,这太难以抉择了。但苹果怎么可能没考虑到这点了,可以通过赠送的耳机转接器 —— 将传统的耳机头转为 lightning:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class HeadsetAdapter implements PhoneJackInterface { // 基于传统耳机接口

LightningInterface lightning; // 兼容新接口

/**
* 传入 lightning 接口
* @param lightning
*/
public HeadsetAdapter(LightningInterface lightning) {
this.lightning = lightning;
}

/**
* 对传统接口兼容
*/
@Override
public void audioTraditionally() {
lightning.audioWithLightning();
}
}

类适配器

这样不够简洁,我们可以改一改:

1
2
3
4
5
6
7
复制代码public class HeadsetAdapter extends LightningConnector implements PhoneJackInterface {
@Override
public void audioTraditionally() {
// 传统接口兼容 lightning
super.audioWithLightning();
}
}

测试:

1
2
3
4
5
复制代码// test
HeadsetAdapter headsetAdapter = new HeadsetAdapter();
Iphone iphone7 = new Iphone(headsetAdapter);
iphone7.play();
// 控制台输出 “通过 Lightning 播放声音”

对象适配器

我们一般将上面的适配器称作「类适配器」,除此之外还有一种 「对象适配器」,我们可以对适配器类进行如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public class ObjectHeadsetAdapter implements PhoneJackInterface { // 基于传统耳机接口

LightningConnector lightning; // 兼容新接口

/**
* 传入 lightning 接口
* @param lightning
*/
public ObjectHeadsetAdapter(LightningConnector lightning) {
this.lightning = lightning;
}

/**
* 对传统接口兼容
*/
@Override
public void audioTraditionally() {
// 使用委托实现兼容
this.lightning.audioWithLightning();
}
}

测试:

1
2
3
复制代码ObjectHeadsetAdapter objectHeadsetAdapter = new ObjectHeadsetAdapter(new LightningConnector());
Iphone iphoneX = new Iphone(objectHeadsetAdapter);
iphoneX.play();

对象适配器 vs 类适配器

通过以上简单的例子,相信你对适配器模式有一个大致了解了。「类适配器」与「对象适配器」的区别概括如下:

- 类适配器 对象适配器
创建方式 需要通过创建自身创建出一个新的 Adapter 可以通过已有的 Adapter 对象来转换接口
扩展性 通过 Override 来扩展新需求 因为包含关系所以不能扩展
其他 继承被适配类,所以相对静态 包含被适配类,所以相对灵活

优点

总的来说,适配器模式主要有以下几个优点:

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  2. 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  3. 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合「开闭原则」。

看完 Java 的实现方式,我们再来看看 Scala 是如何实现的。

Scala 版本

在 Scala 中,由于方便的语法糖,我们并不需要像 Java 那样麻烦,已知传统接口类(此处省略一些接口)

1
2
3
复制代码class PhoneJackConnector {
def audioTraditionally = println("通过 PhoneJack 播放声音")
}

如果我们有需要适配的,为其创建一个 trait 即可:

1
2
3
复制代码trait Lightning {
def audioWithLightning()
}

其次再新建一个类,继承传统类:

1
2
3
4
5
复制代码class HeadsetAdapter extends PhoneJackConnector with Lightning {
override def audioTraditionally: Unit = super.audioTraditionally

override def audioWithLightning: Unit = println("通过 Lightning 播放声音")
}

你会开心的发现:在这个新的类里,我们可以对新老方法一起扩展——在 Java 中,这是「对象适配器」和 「类适配器」比较大的一个劣势。

测试:

1
2
复制代码val headsetAdapter = new HeadsetAdapter
headsetAdapter.audioTraditionally

当然,除了这种方式,Scala 里还可以通过隐式转换来实现适配 final 类的适配器:

1
2
3
4
5
6
7
8
9
复制代码final class FinalPhoneJackConector {
def audioTraditionally = println("通过 PhoneJack 播放声音")
}

object FinalPhoneJackConector {
implicit class ImplictHeadsetAdapter(phoneJackConnector: FinalPhoneJackConector) extends Lightning {
override def audioWithLightning: Unit = println("通过 Lightning 播放声音")
}
}

测试:

1
2
3
4
5
6
复制代码val headsetAdapter = new HeadsetAdapter
headsetAdapter.audioTraditionally

// 隐式
val light: Lightning = new FinalPhoneJackConector
light.audioWithLightning()

Hint: 对于不熟悉 implicit 的朋友可以 看一下这里

总结

光从代码量来说,Scala 简洁比 Java 表现的好太多。

其次,Scala 结合了「类适配器」和「对象适配器」所有的优点,并消除了自身问题。与 Java 相比,Scala 有如下特点:

  1. 与对象适配器一样灵活
  2. 与「类适配器相比」,没有对特定被适配类的依赖
  3. 只适用于不需要动态改变被适配类的情况

源码链接
如有错误和讲述不恰当的地方还请指出,不胜感激!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP应用程序在MVC模式中构建安全API

发表于 2017-11-20

继续工作

在本系列文章的第一部分和第二部分我介绍了一些我们构建API所需要的基础库和基本概念。现在我们将进入本系列文章的第三部分,在这之前,我想再回顾一下第一和第二部分的内容,总结一些可以帮助我们走的更长远的一些东西。我相信你已经注意到(在这个
Git 仓库中查看本系列文章的“第二部分”的分支上的代码)在我们的index.php文件中的代码量有点大。我们已经定义了主应用程序,并为自定义处理程序更改了一些配置选项。即便只是简单的使用这些代码,保存到一个文件里也会变得有点冗长。

使用MVC设计模式

在这个系列的文章中我们实现了很多功能,你可以将这些功能全部保存到一个文件中,不过,这将成为日后进行代码维护的“恶梦”。为了帮助我们解决便于代码维护的问题,我将使用一个用来处理大型应用程序的方法:模型/视图/控制器设计模式。

如果你还不熟悉这种结构,请看下面的简单介绍:

· 模型表示要处理的数据。在大多数数据库驱动的应用程序中,它们将与表直接关联,每个实体类型之间都存在着关系。

· 视图表示应用程序的输出,即客户端的HTML,在我们本系列文章中的API的输出是JSON或XML。

· 控制器是将模型和视图绑定在一起的“粘合剂”,并在将值发送到视图进行输出之前对值进行一些额外的处理。

这种结构的目标是基于单一责任原则将应用程序的功能分解成块。应用程序中的每个类/对象只能做一件事情。还有其他的部分被包含在功能更强大的MVC框架中,如服务提供者和其他业务逻辑处理程序,但我们在这里会使用简单的几个功能。虽然我们现在做的事情会涉及到一些额外的处理,但总的来说,我们将坚持使用纯粹的MVC组件。

我们将通过一些中间件功能扩展这个MVC结构,这一点我们在第一部分中简要的介绍过,让我们创建可重复使用的单用途的功能模块,这样我们就可以在整个系统中重复使用。

从我的朋友那得到的一点帮助

在PHP生态系统中有大量的MVC框架,我们可能会使用其中任何一个来完成我们在这里做的大部分工作。

正如你已经看到的那样,Slim框架为我们的应用程序提供了最主要的“骨架”,使我们能够将URL中的请求路由到正确的功能上。正如它的名字一样,这就是它所带来的所有功能。还有其他一些我们会用到的功能,主要是请求和响应处理。

vlucas/phpdotenv

该库用于从.env文件读取定义的内容(默认为当前目录)。这些.env文件包含你的应用程序的设置,并且可以将应用程序的设置保留在代码之外。然后将它们加载到$_ENV变量中,以便在应用程序中的任何地方都可以轻松引用。

aura/session

默认情况下,Slim是不附带会话处理程序的,使用PHP自己的$_SESSION功能可能会有点混乱。相反,我已经选择使用Aura组件集合中的这个包来帮助会话功能保持简洁。它在$_SESSION内部使用处理程序,所以它仍然使用相同的功能,只是会提供一个友好的界面。

illuminate/database

这是Laravel框架中的数据库组件,这个组件能使数据库表中的数据变得更简单。它是一个ORM(对象关系映射器)工具,它使用ActiveRecord结构来引用数据库中表示的实体和集合。该软件包还包括了我们将用于设置我们的连接的功能——Capsule。

doctrine/dbal

这个库需要使用Laravel数据库组件进行一些手动数据库查询。虽然从一开始可能不需要这个组件,但如果需要更复杂的查询,那么它将会派上用场。

robmorgan/phinx

最后,我们将安装Phinx数据库迁移管理器。这个Illuminate/database包在创建表之后需要处理表的所有事情,但我们仍然需要创建它们。Phinx可以轻松的根据需要运行或回滚迁移,并且比使用一大堆原始SQL语句更不容易出错。

要全部安装以上这些组件,可以执行下面这条简单的命令:

1
2
3
4
5
6
7
> composer require vlucas/phpdotenv aura/session illuminate/database doctrine/dbal robmorgan/phinx
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
[...]
Writing lock file
Generating autoload files

这些软件包存在着许多其他的依赖关系,有几个来自于Symfony和Doctrine。不过不要太担心这些依赖关系。即使他们都与Slim一起安装, vendor/目录的大小也只有11MB,这比起任何其他应用程序来说都比较小。

你可能会问,为什么我们会需要这些程序包?所有这一切难道都不能用简单的PHP和SQL来完成吗?这个问题的答案是,这些程序包使得这些功能的开发更快速,因为它们已经经过很好的测试。

“应用程序”结构

现在让我们开始构建的过程吧,看看我们的应用程序将会是什么样子的,我们成功地移动了所有的东西,现在把它分解成各个功能部件。

1
2
3
4
5
6
7
8
9
10
11
12
App/
--> Controller/
--> Model/
--> View/
--> Middleware/
bootstrap/
--> app.php
--> db.php
--> routes.php
templates/
public/
db/

让我们一起来看看这个结构。我们的主要命名空间是App应用程序文件。这是App/目录下的所有文件,包括控制器,模型和任何可能需要的视图辅助类文件。在bootstrap目录的内部,我们将为我们的应用程序提供主要的配置文件。包括了一些基本的应用程序设置(如系列文章第一部分中的处理程序)和Slim应用程序配置。数据库连接信息将存放在db配置文件中,路由设置将在routes配置文件中。

最后的’templates’目录,可以存放任何我们可能需要的视图模板,该db目录将用于存放Phinx迁移的文件,public是放置了我们的前端控制器index.php文件的目录。

请注意,我们正在使用一个子目录作为文档的根目录。这有助于防止一些安全问题,例如.env中包含的各种敏感信息的文件可以直接在Web中访问。

如果你对这些目录不熟悉,你也不要担心,在文章的后面,我将带你操作每一步,并解释在任何一步中都发生了些什么。

现在要花点时间进行目录的创建:

1
2
3
4
5
mkdir App
mkdir bootstrap
mkdir templates
mkdir public
mkdir db

迁移

现在我们在index.php文件中已经定义了一些代码:

· 应用程序的引导

· 路由处理

· 根路径/请求的请求/响应处理程序

构建bootstrap

我们要把已有的代码进行修改,并把它们分解成我们想要的新结构。首先我们将从bootstrap开始。我们来看一下这个代码,把它移到一个bootstrap/app.php文件中,看起来像这样:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
session_start();
require_once '../vendor/autoload.php';
 
$dotenv = new DotenvDotenv(BASE_PATH);
$dotenv->load();
 
$app = new SlimApp();
 
$container = $app->getContainer();
 
// Make the custom App autoloader
spl_autoload_register(function($class) {
    $classFile = APP_PATH.'/../'.str_replace('', '/', $class).'.php';
    if (!is_file($classFile)) {
        throw new Exception('Cannot load class: '.$class);
    }
    require_once $classFile;
});
 
// Autoload in our controllers into the container
foreach (new DirectoryIterator(APP_PATH.'/Controller') as $fileInfo) {
    if($fileInfo->isDot()) continue;
    $class = 'AppController'.str_replace('.php', '', $fileInfo->getFilename());
    $container[$class] = function($c) use ($class){
        return new $class();
    };
}
 
$container['notFoundHandler'] = function($container) {
    return function ($request, $response) use ($container) {
        return $container['response']
            ->withStatus(404)
            ->withHeader('Content-Type', 'application/json')
            ->write(json_encode(['error' => 'Resource not valid']));
    };
};
 
$container['errorHandler'] = function($container) {
    return function ($request, $response, $exception = null) use ($container) {
        $code = 500;
        $message = 'There was an error';
 
        if ($exception !== null) {
            $code = $exception->getCode();
            $message = $exception->getMessage();
        }
 
        // Use this for debugging purposes
        /*error_log($exception->getMessage().' in '.$exception->getFile().' - ('
            .$exception->getLine().', '.get_class($exception).')');*/
 
        return $container['response']
            ->withStatus($code)
            ->withHeader('Content-Type', 'application/json')
            ->write(json_encode([
                'success' => false,
                'error' => $message
            ]));
    };
};
 
$container['notAllowedHandler'] = function($container) {
    return function ($request, $response) use ($container) {
        return $container['response']
            ->withStatus(401)
            ->withHeader('Content-Type', 'application/json')
            ->write(json_encode(['error' => 'Method not allowed']));
    };
};

这是从我们之前创建的代码中复制粘贴的。在这里,我们正在创建应用程序,获取容器并设置我们的自定义处理程序,用于异常和未找到(404)/不允许(405)的问题。但是,文件开始处有一些额外的代码需要添加。

首先,在我们定义之前,你会注意到SlimApp调用了DotenvDotenv和它的load方法。这个方法会在根目录中的.env查找要加载的文件。我在系列文章中提到过vlucas/phpdotenv这个包,这就是我们使用它的地方。继续往下看,在这个项目的根目录(和public/不是一个级别)中,创建一个名为.env的文件,文件内容如下:

1
2
3
4
DB_HOST=localhost
DB_NAME=database_name
DB_USER=database_user
DB_PASS=database_password

以上内容为我们提供了我们稍后设置数据库连接会用到的更新模板。这些值将在运行时通过Dotenv处理程序加载到$_ENV变量中并在整个应用程序中使用。

如果你忘记了设置.env文件或这个文件位于一个错误的位置,则该程序包会抛出异常,并且你的应用程序将无法继续执行。

接下来让我们来看看自定义自动加载器。由于我们想要在App应用程序的各个部分中引用命名空间中的类,因此我们需要添加一个自定义的自动加载器来处理这些请求。我们利用spl_autoload_register函数来定义这个自动加载器,并使用它的APP_PATH找到匹配的文件。

下面的代码是Slim在使用控制器时需要的东西。正如我之前提到过的,Slim大量使用依赖注入容器来做很多的事情。这当然也包括了当从路由引用时解析控制器和动作方法。在我们的根路由示例中,我们只是直接输出了一些东西,但是可以很容易地转换成如下所示的代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
 
class IndexController
{
    public function index()
    {
        echo 'index!';
    }
}
 
$app->get('/', 'IndexController:index');

上面定义的GET请求路由是Slim用于将HTTP请求正确的路由到IndexController中的index方法。但是,为了实现这一点,我们需要预先加载控制器。DirectoryIterator就是负责预加载的类,它会列出AppController目录的文件并加载到容器中。这样就可以轻松的定义我们的路由了。

编写前置控制器

现在我们将把我们的前置控制器放在public/index.php文件中。因为我们需要从我们的引导文件中引入代码,所以我们将把它包含在文件的起始位置处,并设置一些我们以后可以使用的其他常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
define('BASE_PATH', __DIR__.'/..');
define('APP_PATH', BASE_PATH.'/App');
 
require_once BASE_PATH.'/vendor/autoload.php';
 
// Autorequire everything in BASE_PATH/bootstrap, loading app first - most important
require_once BASE_PATH.'/bootstrap/app.php';
foreach (new DirectoryIterator(BASE_PATH.'/bootstrap') as $fileInfo) {
    if($fileInfo->isDot()) continue;
    require_once $fileInfo->getPathname();
}
 
$app->run();

正如你在上面的代码中看到的,首先我们定义了可以跨应用程序使用的两个常量:BASE_PATH定义了Web应用程序的根目录(和public/是一个级别的), APP_PATH指向根目录下的App/文件夹。下面我们需要使用Composer将 BASE_PATH指向的路径作为源进行自动加载。

再往下一点的代码块会首先加载我们先前创建的引导文件bootstrap/app.php,这个文件定义了应用程序和处理程序。然后,使用DirectoryIterator加载bootstrap/目录中的任何文件。这样我们会在后面就能够更容易的添加更多的配置设置,包括我们的数据库和路由配置,而无需将它们手动包含在引导文件中。

public/index.php示例文件中的最后一行代码是调用应用程序对象上的run方法。这个方法是告诉Slim应该处理传入请求并输出响应(请求生命周期)的方法。

设置请求路由

现在我们已经编写了引导代码和前置控制器,我们需要使用新的MVC结构重新定义默认的/根路由。在bootstrap/目录中创建一个新文件:bootstrap/routes.php。这个文件由我们的bootstrap/app.php自动加载:

1
2
3
<?php
 
$app->get('/', 'AppControllerIndexController:index');

为了重新定义默认的/根路由,需要将/请求指向IndexController。由于我们已经将这些控制器注入到了我们的容器中,因此Slim可以解析这个文件并将其发送到需要的地方。我们稍后会在这个控制器中再添加一些功能。现在我们需要设置一个配置文件和数据库配置。

定义数据库配置

现在我们将创建数据库配置,利用Laravel’s Enloquent包中附带的“Capsule”功能,就可以在Laravel应用程序之外使用Eloquent功能。由于我们已经使用.env文件定义了我们的数据库连接信息,所以我们在这里需要做的是通过一些代码来设置”Capsule”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$dbconfig = [
    'driver'    => 'mysql',
    'host'      => $_ENV['DB_HOST'],
    'database'  => $_ENV['DB_NAME'],
    'username'  => $_ENV['DB_USER'],
    'password'  => $_ENV['DB_PASS'],
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
];
 
$capsule = new IlluminateDatabaseCapsuleManager;
$capsule->addConnection($dbconfig);
$capsule->setAsGlobal();
$capsule->bootEloquent();

我在本教程中使用的是MySQL,但也可以使用其他数据库。请参阅Laravel手册以确定当前支持哪些数据库。在上面的代码中,我们首先从.env文件中定义的$dbconfig数组变量中加载的值来创建数据库配置。将凭证信息保存在环境变量中可以防止敏感信息泄露。

最后,我们通过$capsule对象的addConnection方法创建并传递数据库配置。最后两行代码能够使我们在全局应用程序中无缝地使用Eloquent的功能。

把代码放在一起

我们正在进入这个系列最为重要的部分。由于我们之前已经把一些重要的事情准备好了,所以把这些功能合并起来就比较容易了。

我们先从“base”控制器开始,这个控制器包含了一些简单的方法,然后我们可以在所有的控制器中调用。一些OOP / MVC的纯粹主义者可能会不赞同这个想法。创建一个新的文件AppControllerBaseController.php包含如下代码:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php
namespace AppController;
 
class BaseController
{
    protected $container;
 
    /**
     * Initialize the controller with the container
     *
     * @param SlimContainer $container Container instance
     */
    public function __construct(SlimContainer $container)
    {
        $this->container = $container;
    }
 
    /**
     * Magic method to get things off of the container by referencing
     * them as properties on the current object
     */
    public function __get($property)
    {
        // Special property fetch for user
        if ($property == 'user') {
            return $user = $this->container->get('session')->get('user');
        }
 
        if (isset($this->container, $property)) {
            return $this->container->$property;
        }
        return null;
    }
 
    /**
     * Handle the response and put it into a standard JSON structure
     *
     * @param boolean $status Pass/fail status of the request
     * @param string $message Message to put in the response [optional]
     * @param array $addl Set of additional information to add to the response [optional]
     */
    public function jsonResponse($status, $message = null, array $addl = [])
    {
        $output = ['success' => $status];
        if ($message !== null) {
            $output['message'] = $message;
        }
        if (!empty($addl)) {
            $output = array_merge($output, $addl);
        }
 
        $response = $this->response->withHeader('Content-type', 'application/json');
        $body = $response->getBody();
        $body->write(json_encode($output));
 
        return $response;
    }
 
    /**
     * Handle a failure response
     *
     * @param string $message Message to put in response [optional]
     * @param array $addl Set of additional information to add to the response [optional]
     */
    public function jsonFail($message = null, array $addl = [])
    {
        return $this->jsonResponse(false, $message, $addl);
    }
 
    /**
     * Handle a success response
     *
     * @param string $message Message to put in response [optional]
     * @param array $addl Set of additional information to add to the response [optional]
     */
    public function jsonSuccess($message = null, array $addl = [])
    {
        return $this->jsonResponse(true, $message, $addl);
    }
}

我们的BaseController只是定义了一些辅助方法,例如JSON响应的输出标准化。jsonSuccess和jsonFail只是jsonResponse方法的抽象方法。

另外还定义了__get方法。这是一种PHP魔术方法,当从不存在或不是公开的对象请求属性时将调用此方法。在这种情况下,我们希望能够从容器中获得更多的东西。此外,它还有一些额外的代码,例如让用户注销会话等。

此外,你还将注意到,我们正在使用BaseController的__construct方法接收当前容器的初始化实例。Slim在调用控制器时自动执行此操作,这使得基本控制器和扩展它的类都可以访问到该控制器。

接下来,我们将创建IndexController来处理/请求,所以AppControllerIndexController.php文件的代码如下:

1
2
3
4
5
6
7
8
9
10
<?php
namespace AppController;
 
class IndexController extends AppControllerBaseController
{
    public function index()
    {
        return $this->jsonSuccess('Hello world!');
    }
}

你会注意到我们已经利用jsonSuccess方法返回了一个 “Hello world!” 。

发起请求

现在,一切都已准备就绪,你可以通过简单的HTTP调用来测试调用API的结果。首先,我们使用之前用过的PHP内置的Web服务器来启动应用程序:

1
2
cd public/
php -S localhost:8000

现在你可以在浏览器中访问此地址:http://localhost:8000。如果一切顺利的话,你应该可以看到如下响应:

1
2
3
4
{
    success: true,
    message: "Hello world!"
}

或者,你也可以使用curl来发起请求:

1
2
$ curl http://localhost:8000
{"success":true,"message":"Hello world!"}

写在最后

在这一部分中我做了很多代码重构的事情,并为API应用程序增加了复杂性。我知

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】 Python 37 新特性 Python 37

发表于 2017-11-20
  • 原文地址:What’s New In Python 3.7
  • 原文作者:docs.python.org/
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:winjeysong
  • 校对者:LynnShaw

Python 3.7 新特性

  • 版本:3.7.0a1
  • 日期:2017年9月27日

本文阐述了Python 3.7所具有的新特性(与3.6版本对比)。

详见更新日志。

注意: 预发布版本的用户要留意,本文档目前还属于草案。随着Python 3.7的发布,后续将会有很显著的更新,所以即使阅读过早期版本,也值得再回来看看。

版本亮点总结

新特性

PEP 538:遗留的C语言本地化编码自动强制转换问题

在 Python 3 系列版本中,确定一个合理的默认策略来处理当前位于非 Windows 平台上默认C语言本地化编码隐式采用的“7位 ASCII”,是个永不停歇的挑战。

PEP 538 更新了默认的解释器命令行界面,从而能自动地将本地化编码强制转换为一种可用的且基于 UTF-8的编码,它就是文档里所描述的新环境变量 PYTHONCOERCECLOCALE。用这种方式自动设置 LC_CTYPE 意味着核心解释器和关于本地化识别的C语言扩展(如 readline)将会采用 UTF-8 作为默认的文本编码,而不是 ASCII。

PEP 11 中有关平台支持的定义也已经更新,限制了对于全文处理的支持,变为适当的基于非 ASCII 的本地化编码配置。

作为变化的一部分,当使用任一强制转换的已定义目标编码(当前为 C.UTF-8,C.utf8 和 UTF-8),stdin 及 stdout 的默认错误处理器现在为 surrogateescape(而不是 strict);而 stderr 的默认错误处理器仍然是 backslashreplace,与语言环境无关。

默认的本地化编码强制转换是隐式的,但是为了能帮助调试潜在的与本地化相关的集成问题,可以通过设置 PYTHONCOERCECLOCALE=warn 来请求直接用 stderr 发出明确的警告。当核心解释器初始化时,如果遗留的C语言本地化编码仍是活动状态,那么该设置会导致 Python 运行时发出警告。

另见:

PEP 538 —— 把遗留的C语言本地化编码强制转换为基于 UTF-8 的编码。

PEP 由 Nick Coghlan 撰写及实施。

其他的语言更新

  • 现在传递给某个函数的参数( argument )可以超过255个,且一个函数的形参( parameter )可以超过255个。(由 Serhiy Storchaka 参与贡献的 bpo-12844 和 bpo-18896。)
  • bytes.fromhex() 及 bytearray.fromhex() 现在将忽略所有的 ASCII 空白符,而不止空格。(由 Robert Xiao 参与贡献的 bpo-28927。)
  • 现在当 from ... import ... 失败的时候,ImportError 会展示模块名及模块 __file__ 路径。(由 Matthias Bussonnier 参与贡献的 bpo-29546。)
  • 现在已支持将包含绝对 imports 的循环 imports 通过名称绑定到一个子模块上。(由 Serhiy Storchaka 参与贡献的 bpo-30024。)
  • 现在,object.__format__(x,'') 等价于 str(x) ,而不是 format(str(self),'')。(由 Serhiy Storchaka 参与贡献的 bpo-28974。)

新模块

  • 暂无。

改进的模块

argparse

在大多数的 unix 命令中,parse_intermixed_args() 能让用户在命令行里混用选项和位置参数,它支持大部分而非全部的 argparse 功能。(由 paul.j3 参与贡献的 bpo-14191。)

binascii

b2a_uu() 函数现在能接受一个可选的 backtick 关键字参数,当它的值为 true 时,所有的“0”都将被替换为 '‘` 而非空格。(由 Xiang Zhang 参与贡献的 bpo-30103。)

calendar

HTMLCalendar类具有新的类属性,它能在生成的 HTML 日历中很方便地自定义 CSS 类。(由 Oz Tiram 参与贡献的 bpo-30095。)

cgi

parse_multipart() 作为 FieldStorage 会返回同样的结果:对于非文件字段,与键相关联的值是一个字符串列表,而非字节。(由 Pierre Quentel 参与贡献的 bpo-29979。)

contextlib

已添加 contextlib.asynccontextmanager()。(由 Jelle Zijlstra 参与贡献的 bpo-29679。)

dis

dis() 函数现在可以反汇编嵌套代码对象(代码解析,生成器表达式和嵌套函数,以及用于构建嵌套类的代码)。(由 Serhiy Storchaka 参与贡献的 bpo-11822。)

distutils

README.rst 现已包含在 distutils 的标准自述文件列表中,进而它也分别包含在各源码中。(由 Ryan Gonzalez 参与贡献的 bpo-11913。)

http.server

SimpleHTTPRequestHandler 支持 HTTP If-Modified-Since 头文件。如果在头文件指定的时间之后,目标文件未被修改,则服务器返回 304 响应状态码。 (由 Pierre Quentel 参与贡献的 bpo-29654。)

在 SimpleHTTPRequestHandler 中添加 directory 参数,在命令行的 server 模块中添加 --directory。有了这个参数,服务器将会运行在指定目录下,默认使用当前工作目录。(由 Stéphane Wirtel and Julien Palard 参与贡献的 bpo-28707。)

locale

在 locale 模块的 format_string() 方法中添加了另一个参数 monetary 。如果 monetary 的值为 true,会转换为使用货币千位分隔符和分组字符串。(由 Garvit 参与贡献的 bpo-10379。)

math

新的 remainder() 函数实现了 IEEE 754-style 的取余操作。(由 Mark Dickinson 参与贡献的 bpo-29962。)

os

增加了对 fwalk() 中 bytes 路径的支持。(由 Serhiy Storchaka 参与贡献的 bpo-28682。)
(Contributed by Serhiy Storchaka in bpo-28682.)

在Unix平台上,增加了对 scandir() 中 file descriptors 的支持。(由 Serhiy Storchaka 参与贡献的 bpo-25996。)

新的 os.register_at_fork() 函数允许注册 Python 的回调在进程的分支上执行。(由 Antoine Pitrou 参与贡献的 bpo-16500。)

pdb

set_trace() 现在需要一个可选的 header 强制关键字参数。如果已给出,它将会在调试开始前打印至控制台。

string

string.Template 现在可以分别为花括号占位符和非花括号占位符选择性地修改正则表达式模式。(由 Barry Warsaw 参与贡献的 bpo-1198569。)

unittest.mock

sentinel 属性现在会保留自己的同一性,当它们被 copied 或 pickled 时。(由 Serhiy Storchaka 参与贡献的 bpo-20804。)

xmlrpc.server

xmlrpc.server.SimpleXMLRPCDispatcher 的 register_function() 及其子类能被用作装饰器。(由 Xiang Zhang 参与贡献的 bpo-7769。)

unicodedata

内部的 unicodedata 数据库已升级,能够使用 Unicode 10。 (由 Benjamin Peterson 参与贡献。)

urllib.parse

urllib.parse.quote() 已经从 RFC 2396 升级至 RFC 3986,将 ~ 添加到默认情况下从不引用的字符集中。(由 Christian Theune 和 Ratnadeep Debnath 参与贡献的 bpo-16285。)

uu

函数 encode() 现在能接受一个可选的关键字参数 backtick ,当它的值为 true 时,“0”会被 '‘` 替代而非空格。(由 Xiang Zhang 参与贡献的 bpo-30103。)

zipapp

函数 zipapp.create_archive() 现在能接受一个可选的参数 filter,来允许用户选择哪些文件应该被包含在存档中。

优化

  • 添加了两个新的操作码:LOAD_METHOD 及 CALL_METHOD,从而避免为了方法调用的绑定方法对象的实例化,这将导致方法调用的速度提升20%。(由 Yury Selivanov 及 INADA Naoki 参与贡献的 bpo-26110。)
  • 当在一字符串内查找某些特殊的 Unicode 字符(如乌克兰大写字母 “Є”)时,将会比查找其他字符慢25倍,但现在最差情况下也只慢了3倍。(由 Serhiy Storchaka 参与贡献的 bpo-24821。)
  • 标准C语言库的快速执行现在能用于 math 模块内的 erf() 和 erfc() 函数。(由 Serhiy Storchaka 参与贡献的 bpo-26121。)
  • 由于使用了 os.scandir() 函数,os.fwalk() 函数的效率已经提升了2倍。 (由 Serhiy Storchaka 参与贡献的 bpo-25996。)
  • 优化了对于大小写忽略的匹配及对于 regular expressions 的查找。 对一些字符的查找速度现在能提升至原来的20倍。(由 Serhiy Storchaka 参与贡献的 bpo-30285。)
  • 在较重负荷下,selectors.EpollSelector.modify(),selectors.PollSelector.modify() 及 selectors.DevpollSelector.modify() 将比原来快10%左右。(由 Giampaolo Rodola’ 参与贡献的 bpo-30014。)

编译生成及C语言API的更改

  • 在非OSX、UNIX平台上,当构建 _ctypes 模块时不会再打包 libffi 的完整副本以使用。现在在这些平台上构建 _ctypes 时需要已安装的 libffi 副本。(由 Zachary Ware 参与贡献的 bpo-27979。)
  • 结构 PyMemberDef,PyGetSetDef,PyStructSequence_Field,PyStructSequence_Desc 及 wrapperbase 的 name 和 doc 字段的类型现在为 const char * 而非 char *。(由 Serhiy Storchaka 参与参与贡献的 bpo-28761。)
  • PyUnicode_AsUTF8AndSize() 及 PyUnicode_AsUTF8() 返回的类型是 const char * 而非 char *。(由 Serhiy Storchaka 参与贡献的 bpo-28769。)
  • 新增了函数 PySlice_Unpack() 和 PySlice_AdjustIndices()。 (由 Serhiy Storchaka 参与贡献的 bpo-27867。)
  • 已弃用 PyOS_AfterFork(),支持使用新函数 PyOS_BeforeFork(),PyOS_AfterFork_Parent() 及 PyOS_AfterFork_Child()。 (由 by Antoine Pitrou 参与贡献的 bpo-16500。)
  • Windows 构建进程不再依赖 Subversion 来 pull 外部资源,而是通过使用 Python 脚本从 Github 下载 zip 文件。如果系统未安装 Python 3.6(通过命令 py -3.6),将会使用 NuGet 来下载 32位的 Python 副本。(由 Zachary Ware 参与贡献的 bpo-30450。)
  • 移除了对于构建 --without-threads 的支持。(由 Antoine Pitrou 参与贡献的 bpo-31370。)

其他 CPython 实现的更改

  • 在被追踪的框架上,通过将新的 f_trace_lines 属性设置为 False,追踪钩子现在可以选择不接收来自解释器的 line 事件。(由 Nick Coghlan 参与贡献的 bpo-31344。)
  • 在被追踪的框架上,通过将新的 f_trace_opcodes 属性设置为 True,追踪钩子现在可以选择接收来自解释器的 opcode 事件。(由 Nick Coghlan 参与贡献的 bpo-31344。)

弃用的内容

  • 如果未设置 Py_LIMITED_API ,或其被设置为从 0x03050400 到 0x03060000 (不含)的值或不小于 0x03060100 的值,将弃用函数 PySlice_GetIndicesEx() 并用宏将其替代。(由 Serhiy Storchaka 参与贡献的 bpo-27867。)
  • 用 format_string() 来替代 locale 模块中被弃用的 format()。(由 Garvit 参与贡献的 bpo-10379。)
  • 方法 MetaPathFinder.find_module()(由 MetaPathFinder.find_spec() 替代)和方法 PathEntryFinder.find_loader()(由 PathEntryFinder.find_spec() 替代)都已在 Python 3.4 被弃用,且现在会发出 DeprecationWarning的警告。(由 Matthias Bussonnier 参与贡献的 bpo-29576。)
  • 在 gettext 中通过使用非整型值来筛选复数形式的值已被弃用,它不会再起作用。(由 Serhiy Storchaka 参与贡献的 bpo-28692。)
  • macpath 模块已被弃用,且它将会在 Python 3.8 版本中被移除。

C语言API的更改

  • PyThread_start_new_thread() 和 PyThread_get_thread_ident() 返回结果的类型, 及 PyThreadState_SetAsyncExc() 中参数 id 的类型从 long 变为 unsigned long。(由 Serhiy Storchaka 参与贡献的 bpo-6532。)
  • 如果 PyUnicode_AsWideCharString() 的第二个实参是 NULL 且 wchar_t* 字符串包含空字符,就会引起一个 ValueError 的报错。(由 Serhiy Storchaka 参与贡献的 bpo-30708。)

仅Windows平台

  • Python 启动器(py.exe)能接收32及64位说明符,且无需指定次要版本。所以 py -3-32 与 py -3-64 也会和 py -3.7-32 一样有效,并且现在能接受 -m-64 与 -m.n-64 来强制使用64位 Python,即使32位在使用中也是如此。如果指定版本不可用,py.exe将会报错退出。(由 Steve Barnes 参与贡献的 bpo-30291。)
  • 启动器可以通过命令 “py -0” 运行,生成已安装 Python 的版本列表,标有星号的是为默认,运行 “py -0p” 将包含安装路径。如果 py 使用无法匹配的版本说明符运行,也会打印缩略形式的可用说明符列表。(由 Steve Barnes 参与贡献的 bpo-30362。)

移除的内容

移除的API及特性

  • 在使用 re.sub() 的替换模板中,由 '\' 及一个 ASCII 字母组成的未知转义符已在 Python 3.5 中被弃用,现在使用将会报错。
  • 移除了 tarfile.TarFile.add() 中的实参 exclude 。它已在 Python 2.7 和 3.2 版本被弃用,取而代之的是使用实参 filter。
  • ntpath 模块中的 splitunc() 函数在 Python 3.1 被弃用,现在已被移除。使用 splitdrive() 函数来替代。
  • collections.namedtuple() 不再支持 verbose 参数和 _source 属性,该属性用于显示为已命名元组类所生成的源码。这是用来提升类创建速度的优化设计的一部分。(由 Jelle Zijlstra 贡献并由 INADA Naoki,Serhiy Storchaka,和 Raymond Hettinger 进一步完善的 bpo-28638。)
  • 函数 bool(),float(),list() 和 tuple() 不再使用关键字参数。int() 的第一个参数现在只能作为位置参数传递。
  • 移除了先前在 Python 2.4 版本已被弃用的在 plistlib 模块中的类 Plist,Dict 和 _InternalDict。函数 readPlist() 和 readPlistFromBytes() 返回结果中的 dict 类型值现在就是标准的 dict 类型。你再也不能使用属性访问来访问到这些字典里的项。

移植到 Python 3.7

本小节列出了之前描述的一些更改,以及一些其他bug修复,因而你可能需要对你的代码进行更改。

Python API的更改

  • 如果 path 是一个字符串,pkgutil.walk_packages() 现在会引起 ValueError 报错,之前会返回一个空列表。(由 Sanyam Khurana 参与贡献的 bpo-24744。)
  • string.Formatter.format() 的格式化字符串参数现在是 positional-only,将它作为关键字参数传递已在 Python 3.5 时被弃用。(由 Serhiy Storchaka 参与贡献的 bpo-29193。)
  • http.cookies.Morsel 类的属性 key,value 和 coded_value 现在是只读的,将值分配给它们已经在 Python 3.5 中被弃用了,需要使用 set() 方法对它们进行设置。(由 Serhiy Storchaka 参与贡献的 bpo-29192。)
  • Module,FunctionDef,AsyncFunctionDef 及 ClassDef AST 节点现在新增了一个 docstring 字段,它们自身的首次声明不再被当做是一个 docstring。类和模块的代码对象 co_firstlineno 及 co_lnotab 会因这个更改而受到影响。(由 INADA Naoki and Eugene Toder 参与贡献的 bpo-29463。)
  • os.makedirs() 的参数 mode 不再影响新建的中级目录的文件权限位,要想设置它们的文件权限位,你可以在调用 makedirs() 之前设置 umask。(由 Serhiy Storchaka 参与贡献的 bpo-19930。)
  • 现在 struct.Struct.format 的类型是 str 而非 bytes。(由 Victor Stinner 参与贡献的 bpo-21071。)
  • 由于 socket 模块的内部更改,你将无法在旧版本 Python 中通过 socket.fromshare() 创建一个 share()-ed(共享的)接口。
  • datetime.timedelta 的 repr 已变为在输出中包含关键字参数。(由 Utkarsh Upadhyay 参与贡献的 bpo-30302。)

CPython 字节码的更改

  • 增加了两个新的操作码:LOAD_METHOD 和 CALL_METHOD。(由 Yury Selivanov 和 INADA Naoki 参与贡献的 bpo-26110。)

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

通过构建一个简单的掷骰子游戏去学习怎么用 Python 编程

发表于 2017-11-20

不论是经验丰富的老程序员,还是没有经验的新手,Python 都是一个非常好的编程语言。

Python 是一个非常流行的编程语言,它可以用于创建桌面应用程序、3D 图形、视频游戏、甚至是网站。它是非常好的首选编程语言,因为它易于学习,不像一些复杂的语言,比如,C、 C++、 或 Java。 即使如此, Python 依然也是强大且健壮的,足以创建高级的应用程序,并且几乎适用于所有使用电脑的行业。不论是经验丰富的老程序员,还是没有经验的新手,Python
都是一个非常好的编程语言。

安装 Python

在学习 Python 之前,你需要先去安装它:

Linux: 如果你使用的是 Linux 系统, Python 是已经包含在里面了。但是,你如果确定要使用 Python 3 。应该去检查一下你安装的 Python 版本,打开一个终端窗口并输入:

1
复制代码python3 -V

如果提示该命令没有找到,你需要从你的包管理器中去安装 Python 3。

MacOS: 如果你使用的是一台 Mac,可以看上面 Linux 的介绍来确认是否安装了 Python 3。MacOS 没有内置的包管理器,因此,如果发现没有安装 Python 3,可以从 python.org/downloads/m… 安装它。即使 macOS 已经安装了 Python
2,你还是应该学习 Python 3。

Windows: 微软 Windows 当前是没有安装 Python 的。从 python.org/downloads/w… 安装它。在安装向导中一定要选择 Add Python to PATH 来将 Python 执行程序放到搜索路径。

在 IDE 中运行

在 Python 中写程序,你需要准备一个文本编辑器,使用一个集成开发环境(IDE)是非常实用的。IDE 在一个文本编辑器中集成了一些方便而有用的 Python 功能。IDLE 3 和 NINJA-IDE 是你可以考虑的两种选择:

IDLE 3

Python 自带的一个基本的 IDE 叫做 IDLE。

IDLE

它有关键字高亮功能,可以帮助你检测拼写错误,并且有一个“运行”按钮可以很容易地快速测试代码。

要使用它:

  • 在 Linux 或 macOS 上,启动一个终端窗口并输入 idle3。
  • 在 Windows,从开始菜单中启动 Python 3。
  • 如果你在开始菜单中没有看到 Python,在开始菜单中通过输入 cmd 启动 Windows 命令提示符,然后输入 C:\Windows\py.exe。
  • 如果它没有运行,试着重新安装 Python。并且确认在安装向导中选择了 “Add Python to PATH”。参考 docs.python.org/3/using/win… 中的详细介绍。
  • 如果仍然不能运行,那就使用 Linux 吧!它是免费的,只要将你的 Python 文件保存到一个 U 盘中,你甚至不需要安装它就可以使用。

Ninja-IDE

Ninja-IDE 是一个优秀的 Python IDE。它有关键字高亮功能可以帮助你检测拼写错误、引号和括号补全以避免语法错误,行号(在调试时很有帮助)、缩进标记,以及运行按钮可以很容易地进行快速代码测试。

Ninja-IDE

要使用它:

  1. 安装 Ninja-IDE。如果你使用的是 Linux,使用包管理器安装是非常简单的;否则, 从 NINJA-IDE 的网站上 下载 合适的安装版本。
  2. 启动 Ninja-IDE。
  3. 转到 Edit 菜单,并选择 Preferences 设置。
  4. 在 Preferences 窗口中,点击 Execution 选项卡。
  5. 在 Execution 选项卡上,更改 python 为 python3。

Ninja-IDE 中的 Python3

告诉 Python 想做什么

关键字可以告诉 Python 你想要做什么。不论是在 IDLE 还是在 Ninja 中,转到 File 菜单并创建一个新文件。对于 Ninja 用户:不要创建一个新项目,仅创建一个新文件。

在你的新的空文件中,在 IDLE 或 Ninja 中输入以下内容:

1
复制代码print("Hello world.")
  • 如果你使用的是 IDLE,转到 Run 菜单并选择 Run module 选项。
  • 如果你使用的是 Ninja,在左侧按钮条中点击 Run File 按钮。

在 Ninja 中运行文件

关键字 print 告诉 Python 去打印输出在圆括号中引用的文本内容。

虽然,这并不是特别刺激。在其内部, Python 只能访问基本的关键字,像 print、 help,最基本的数学函数,等等。

可以使用 import 关键字加载更多的关键字。在 IDLE 或 Ninja 中开始一个新文件,命名为 pen.py。

警告:不要命名你的文件名为 turtle.py,因为名为 turtle.py 的文件是包含在你正在控制的 turtle (海龟)程序中的。命名你的文件名为 turtle.py ,将会把 Python 搞糊涂,因为它会认为你将导入你自己的文件。

在你的文件中输入下列的代码,然后运行它:

1
复制代码import turtle

Turtle 是一个非常有趣的模块,试着这样做:

1
2
3
4
5
6
7
8
9
复制代码   turtle.begin_fill()
turtle.forward(100)
turtle.left(90)
turtle.forward(100)
turtle.left(90)
turtle.forward(100)
turtle.left(90)
turtle.forward(100)
turtle.end_fill()

看一看你现在用 turtle 模块画出了一个什么形状。

要擦除你的海龟画图区,使用 turtle.clear() 关键字。想想看,使用 turtle.color("blue") 关键字会出现什么情况?

尝试更复杂的代码:

1
2
3
4
5
6
7
8
9
10
11
复制代码    import turtle as t
import time
t.color("blue")
t.begin_fill()
counter=0
while counter < 4:
t.forward(100)
t.left(90)
counter = counter+1
t.end_fill()
time.sleep(5)

运行完你的脚本后,是时候探索更有趣的模块了。

通过创建一个游戏来学习 Python

想学习更多的 Python 关键字,和用图形编程的高级特性,让我们来关注于一个游戏逻辑。在这个教程中,我们还将学习一些关于计算机程序是如何构建基于文本的游戏的相关知识,在游戏里面计算机和玩家掷一个虚拟骰子,其中掷的最高的是赢家。

规划你的游戏

在写代码之前,最重要的事情是考虑怎么去写。在他们写代码 之前,许多程序员是先 写简单的文档,这样,他们就有一个编程的目标。如果你想给这个程序写个文档的话,这个游戏看起来应该是这样的:

  1. 启动掷骰子游戏并按下 Return 或 Enter 去掷骰子
  2. 结果打印在你的屏幕上
  3. 提示你再次掷骰子或者退出

这是一个简单的游戏,但是,文档会告诉你需要做的事很多。例如,它告诉你写这个游戏需要下列的组件:

  • 玩家:你需要一个人去玩这个游戏。
  • AI:计算机也必须去掷,否则,就没有什么输或赢了
  • 随机数:一个常见的六面骰子表示从 1-6 之间的一个随机数
  • 运算:一个简单的数学运算去比较一个数字与另一个数字的大小
  • 一个赢或者输的信息
  • 一个再次玩或退出的提示

制作掷骰子游戏的 alpha 版

很少有程序,一开始就包含其所有的功能,因此,它们的初始版本仅实现最基本的功能。首先是几个定义:

变量是一个经常要改变的值,它在 Python 中使用的非常多。每当你需要你的程序去“记住”一些事情的时候,你就要使用一个变量。事实上,运行于代码中的信息都保存在变量中。例如,在数学方程式 x + 5 = 20 中,变量是 x ,因为字母 x 是一个变量占位符。

整数是一个数字, 它可以是正数也可以是负数。例如,1 和 -1 都是整数,因此,14、21,甚至 10947 都是。

在 Python 中变量创建和使用是非常容易的。这个掷骰子游戏的初始版使用了两个变量: player 和 ai。

在命名为 dice_alpha.py 的新文件中输入下列代码:

1
2
3
4
5
6
7
复制代码    import random
player = random.randint(1,6)
ai = random.randint(1,6)
if player > ai :
print("You win") # notice indentation
else:
print("You lose")

启动你的游戏,确保它能工作。

这个游戏的基本版本已经工作的非常好了。它实现了游戏的基本目标,但是,它看起来不像是一个游戏。玩家不知道他们摇了什么,电脑也不知道摇了什么,并且,即使玩家还想玩但是游戏已经结束了。

这是软件的初始版本(通常称为 alpha 版)。现在你已经确信实现了游戏的主要部分(掷一个骰子),是时候该加入到程序中了。

改善这个游戏

在你的游戏的第二个版本中(称为 beta 版),将做一些改进,让它看起来像一个游戏。

1、 描述结果

不要只告诉玩家他们是赢是输,他们更感兴趣的是他们掷的结果。在你的代码中尝试做如下的改变:

1
2
3
4
复制代码    player = random.randint(1,6)
print("You rolled " + player)
ai = random.randint(1,6)
print("The computer rolled " + ai)

现在,如果你运行这个游戏,它将崩溃,因为 Python 认为你在尝试做数学运算。它认为你试图在 player 变量上加字母 You rolled ,而保存在其中的是数字。

你必须告诉 Python 处理在 player 和 ai 变量中的数字,就像它们是一个句子中的单词(一个字符串)而不是一个数学方程式中的一个数字(一个整数)。

在你的代码中做如下的改变:

1
2
3
4
复制代码    player = random.randint(1,6)
print("You rolled " + str(player) )
ai = random.randint(1,6)
print("The computer rolled " + str(ai) )

现在运行你的游戏将看到该结果。

2、 让它慢下来

计算机运行的非常快。人有时可以很快,但是在游戏中,产生悬念往往更好。你可以使用 Python 的 time 函数,在这个紧张时刻让你的游戏慢下来。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    import random
import time
player = random.randint(1,6)
print("You rolled " + str(player) )
ai = random.randint(1,6)
print("The computer rolls...." )
time.sleep(2)
print("The computer has rolled a " + str(player) )
if player > ai :
print("You win") # notice indentation
else:
print("You lose")

启动你的游戏去测试变化。

3、 检测关系

如果你多玩几次你的游戏,你就会发现,即使你的游戏看起来运行很正确,它实际上是有一个 bug 在里面:当玩家和电脑摇出相同的数字的时候,它就不知道该怎么办了。

去检查一个值是否与另一个值相等,Python 使用 ==。那是个“双”等号标记,不是一个。如果你仅使用一个,Python 认为你尝试去创建一个新变量,但是,实际上你是去尝试做数学运算。

当你想有比两个选项(即,赢或输)更多的选择时,你可以使用 Python 的 elif 关键字,它的意思是“否则,如果”。这允许你的代码去检查,是否在“许多”结果中有一个是 true, 而不是只检查“一个”是 true。

像这样修改你的代码:

1
2
3
4
5
6
复制代码    if player > ai :
print("You win") # notice indentation
elif player == ai:
print("Tie game.")
else:
print("You lose")

多运行你的游戏几次,去看一下你能否和电脑摇出一个平局。

编写最终版

你的掷骰子游戏的 beta 版的功能和感觉比起 alpha 版更像游戏了,对于最终版,让我们来创建你的第一个 Python 函数。

函数是可以作为一个独立的单元来调用的一组代码的集合。函数是非常重要的,因为,大多数应用程序里面都有许多代码,但不是所有的代码都只运行一次。函数可以启用应用程序并控制什么时候可以发生什么事情。

将你的代码变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码    import random
import time
def dice():
player = random.randint(1,6)
print("You rolled " + str(player) )
ai = random.randint(1,6)
print("The computer rolls...." )
time.sleep(2)
print("The computer has rolled a " + str(player) )
if player > ai :
print("You win") # notice indentation
else:
print("You lose")
print("Quit? Y/N")
cont = input()
if cont == "Y" or cont == "y":
exit()
elif cont == "N" or cont == "n":
pass
else:
print("I did not understand that. Playing again.")

游戏的这个版本,在他们玩游戏之后会询问玩家是否退出。如果他们用一个 Y 或 y 去响应, Python 就会调用它的 exit 函数去退出游戏。

更重要的是,你将创建一个称为 dice 的你自己的函数。这个 dice 函数并不会立即运行,事实上,如果在这个阶段你尝试去运行你的游戏,它不会崩溃,但它也不会正式运行。要让 dice 函数真正运行起来做一些事情,你必须在你的代码中去调用它。

在你的现有代码下面增加这个循环,前两行就是上文中的前两行,不需要再次输入,并且要注意哪些需要缩进哪些不需要。要注意缩进格式。

1
2
3
4
5
6
7
复制代码        else:
print("I did not understand that. Playing again.")
# main loop
while True:
print("Press return to roll your die.")
roll = input()
dice()

while True 代码块首先运行。因为 True 被定义为总是真,这个代码块将一直运行,直到 Python 告诉它退出为止。

while True 代码块是一个循环。它首先提示用户去启动这个游戏,然后它调用你的 dice 函数。这就是游戏的开始。当 dice 函数运行结束,根据玩家的回答,你的循环再次运行或退出它。

使用循环来运行程序是编写应用程序最常用的方法。循环确保应用程序保持长时间的可用,以便计算机用户使用应用程序中的函数。

下一步

现在,你已经知道了 Python 编程的基础知识。这个系列的下一篇文章将描述怎么使用 PyGame 去编写一个视频游戏,一个比 turtle 模块有更多功能的模块,但它也更复杂一些。


作者简介:

Seth Kenlon - 一个独立的多媒体大师,自由文化的倡导者,和 UNIX 极客。他同时从事电影和计算机行业。他是基于 slackwarers 的多媒体制作项目的维护者之一, slackermedia.info


via: opensource.com/article/17/…

作者:Seth Kenlon 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务MySQL分库分表数据到MongoDB同步方案

发表于 2017-11-20

需求背景

近年来,微服务概念持续火热,网络上针对微服务和单体架构的讨论也是越来越多,面对日益增长的业务需求是,很多公司做技术架构升级时优先选用微服务方式。我所在公司也是选的这个方向来升级技术架构,以支撑更大访问量和更方便的业务扩展。

发现问题

微服务拆分主要分两种方式:拆分业务系统不拆分数据库,拆分业务系统拆分库。如果数据规模小的话大可不必拆分数据库,因为拆分数据看必将面对多维度数据查询,跨进程之间的事务等问题。而我所在公司随着业务发展单数据库实例已经不能满足业务需要,所以选择了拆分业务系统同时拆分数据库的模式,所以也面临着以上的问题。本文主要介绍多维度数据实时查询解决方案。当前系统架构和存储结构如下:

image

解决思路

  • 要对多数据库数据进行查询,首先就需要将数据库同步到一起以方便查询
  • 为了满足大数据量数据需求,所以优先选择NOSQL数据库做同步库
  • NOSQL数据库基本无法进行关联查询,所以需要将关系数据进行拼接操作,转换成非关系型数据
  • 业务多维度查询需要实时性,所以需要选择NOSQL中实时性相对比较好的数据库:MongoDB

根据以上思路,总结数据整合架构如下图所示:

image

解决方案

目前网上一些数据同步案例分两种:MQ消息同步和binlog数据读取同步

先说MQ消息同步,该同步方式我所在公司试用过一段时间,发现以下问题:

  • 数据围绕业务进行,对业务关键性数据操作发送MQ消息,对业务系统依赖性比较高
  • 对于数据库中存量数据需要单独处理
  • 对于工具表还需要单独维护同步
  • 每次新增数据表都需要重新添加MQ逻辑

考虑到以上问题,用MQ方式同步数据并不是最优解决办法

使用binlog 数据读取方式目前有一些成熟方案,比如tungsten replicator,但这些同步工具只能实现数据1:1复制,数据复制过程自定义逻辑添加比较麻烦,不支持分库分表数据归集操作。综上所述,最优方案应该是读取后binlog后自行处理后续数据逻辑。目前binlog读取binlog工具中最成熟的方案应该就是alibaba开源的canal了。

canal

canal是阿里巴巴mysql数据库binlog的增量订阅&消费组件 。阿里云DRDS、阿里巴巴TDDL 二级索引、小表复制. 都是基于canal做的,应用广泛。canal原理相对比较简单:

  • canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  • mysql master收到dump请求,开始推送binary log给slave(也就是canal)
  • canal解析binary log对象(原始为byte流)

canal介绍: github.com/alibaba/can…

我使用的是canal的HA模式,由zookeeper选举可用实例,每个数据库一个instance,服务端配置如下:

目录:

1
2
3
4
5
6
复制代码conf
database1
-instance.properties
database2
-instance.properties
canal.properties

instance.properties

1
2
3
4
5
6
7
8
9
10
11
复制代码canal.instance.mysql.slaveId = 1001
canal.instance.master.address = X.X.X.X:3306
canal.instance.master.journal.name =
canal.instance.master.position =
canal.instance.master.timestamp =
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8
canal.instance.filter.regex = .*\\..*
canal.instance.filter.black.regex =

canal.properties

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
26
27
28
29
30
31
32
33
34
35
复制代码canal.id= 1
canal.ip=X.X.X.X
canal.port= 11111
canal.zkServers=X.X.X.X:2181,X.X.X.X:2181,X.X.X.X:2181
canal.zookeeper.flush.period = 1000
canal.file.data.dir = ${canal.conf.dir}
canal.file.flush.period = 1000
canal.instance.memory.buffer.size = 16384
canal.instance.memory.buffer.memunit = 1024
canal.instance.memory.batch.mode = MEMSIZE
canal.instance.detecting.enable = true
canal.instance.detecting.sql = select 1
canal.instance.detecting.interval.time = 3
canal.instance.detecting.retry.threshold = 3
canal.instance.detecting.heartbeatHaEnable = false
canal.instance.transaction.size = 1024
canal.instance.fallbackIntervalInSeconds = 60
canal.instance.network.receiveBufferSize = 16384
canal.instance.network.sendBufferSize = 16384
canal.instance.network.soTimeout = 30
canal.instance.filter.query.dcl = true
canal.instance.filter.query.dml = false
canal.instance.filter.query.ddl = false
canal.instance.filter.table.error = false
canal.instance.filter.rows = false
canal.instance.binlog.format = ROW,STATEMENT,MIXED
canal.instance.binlog.image = FULL,MINIMAL,NOBLOB
canal.instance.get.ddl.isolation = false
canal.destinations= example,p4-test
canal.conf.dir = ../conf
canal.auto.scan = true
canal.auto.scan.interval = 5
canal.instance.global.mode = spring
canal.instance.global.lazy = false
canal.instance.global.spring.xml = classpath:spring/default-instance.xml

部署数据流如下:

image

tip:
虽然canal同时支持mixed和row类型的binlog日志,但是获取行数据时如果是mixed类型的日志则获取不到表名,所以本方案暂只支持row格式的binlog

数据同步

创建canal client应用订阅canal读取的binlog数据

1.开启多instance 订阅,订阅多个instance

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
26
27
28
29
> 复制代码public void initCanalStart() {
> List<String> destinations = canalProperties.getDestination();
> final List<CanalClient> canalClientList = new ArrayList<>();
> if (destinations != null && destinations.size() > 0) {
> for (String destination : destinations) {
> // 基于zookeeper动态获取canal server的地址,建立链接,其中一台server发生crash,可以支持failover
> CanalConnector connector = CanalConnectors.newClusterConnector(canalProperties.getZkServers(), destination, "", "");
> CanalClient client = new CanalClient(destination, connector);
> canalClientList.add(client);
> client.start();
> }
> }
> Runtime.getRuntime().addShutdownHook(new Thread() {
> public void run() {
> try {
> logger.info("## stop the canal client");
> for (CanalClient canalClient : canalClientList) {
> canalClient.stop();
> }
> } catch (Throwable e) {
> logger.warn("##something goes wrong when stopping canal:", e);
> } finally {
> logger.info("## canal client is down.");
> }
> }
> });
> }
>
>

订阅消息处理

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
26
27
> 复制代码private void process() {
> int batchSize = 5 * 1024;
> while (running) {
> try {
> MDC.put("destination", destination);
> connector.connect();
> connector.subscribe();
> while (running) {
> Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
> long batchId = message.getId();
> int size = message.getEntries().size();
> if (batchId != -1 && size > 0) {
> saveEntry(message.getEntries());
> }
> connector.ack(batchId); // 提交确认
> // connector.rollback(batchId); // 处理失败, 回滚数据
> }
> } catch (Exception e) {
> logger.error("process error!", e);
> } finally {
> connector.disconnect();
> MDC.remove("destination");
> }
> }
> }
>
>

根据数据库事件处理消息,过滤消息列表,对数据变动进行处理,用到信息为:

  • insert :schemaName,tableName,beforeColumnsList
  • update :schemaName,tableName,afterColumnsList
  • delete :schemaName,tableName,afterColumnsList
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
26
27
28
29
30
31
> 复制代码RowChange rowChage = null;
> try {
> rowChage = RowChange.parseFrom(entry.getStoreValue());
> } catch (Exception e) {
> throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
> }
> EventType eventType = rowChage.getEventType();
> logger.info(row_format,
> entry.getHeader().getLogfileName(),
> String.valueOf(entry.getHeader().getLogfileOffset()), entry.getHeader().getSchemaName(),
> entry.getHeader().getTableName(), eventType,
> String.valueOf(entry.getHeader().getExecuteTime()), String.valueOf(delayTime));
> if (eventType == EventType.QUERY || rowChage.getIsDdl()) {
> logger.info(" sql ----> " + rowChage.getSql());
> continue;
> }
> DataService dataService = SpringUtil.getBean(DataService.class);
> for (RowData rowData : rowChage.getRowDatasList()) {
> if (eventType == EventType.DELETE) {
> dataService.delete(rowData.getBeforeColumnsList(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName());
> } else if (eventType == EventType.INSERT) {
> dataService.insert(rowData.getAfterColumnsList(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName());
> } else if (eventType == EventType.UPDATE) {
> dataService.update(rowData.getAfterColumnsList(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName());
> } else {
> logger.info("未知数据变动类型:{}", eventType);
> }
> }
> }
>
>

ColumnsList转换成MongoTemplate 可用的数据类:DBObject,顺便做下数据类型转换

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
> 复制代码public static DBObject columnToJson(List<CanalEntry.Column> columns) {
> DBObject obj = new BasicDBObject();
> try {
> for (CanalEntry.Column column : columns) {
> String mysqlType = column.getMysqlType();
> //int类型,长度11以下为Integer,以上为long
> if (mysqlType.startsWith("int")) {
> int lenBegin = mysqlType.indexOf('(');
> int lenEnd = mysqlType.indexOf(')');
> if (lenBegin > 0 && lenEnd > 0) {
> int length = Integer.parseInt(mysqlType.substring(lenBegin + 1, lenEnd));
> if (length > 10) {
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : Long.parseLong(column.getValue()));
> continue;
> }
> }
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : Integer.parseInt(column.getValue()));
> } else if (mysqlType.startsWith("bigint")) {
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : Long.parseLong(column.getValue()));
> } else if (mysqlType.startsWith("decimal")) {
> int lenBegin = mysqlType.indexOf('(');
> int lenCenter = mysqlType.indexOf(',');
> int lenEnd = mysqlType.indexOf(')');
> if (lenBegin > 0 && lenEnd > 0 && lenCenter > 0) {
> int length = Integer.parseInt(mysqlType.substring(lenCenter + 1, lenEnd));
> if (length == 0) {
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : Long.parseLong(column.getValue()));
> continue;
> }
> }
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : Double.parseDouble(column.getValue()));
> } else if (mysqlType.equals("datetime") || mysqlType.equals("timestamp")) {
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : DATE_TIME_FORMAT.parse(column.getValue()));
> } else if (mysqlType.equals("date")) {
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : DATE_FORMAT.parse(column.getValue()));
> } else if (mysqlType.equals("time")) {
> obj.put(column.getName(), StringUtils.isBlank(column.getValue()) ? null : TIME_FORMAT.parse(column.getValue()));
> } else {
> obj.put(column.getName(), column.getValue());
> }
> }
> } catch (ParseException e) {
> e.printStackTrace();
> }
> return obj;
> }
>
>

tip:
DBObject对象如果同时用于保存原始数据和组合数据或其他数据,使用时应该深度拷贝对象生成副本,然后使用副本

数据拼接

我们获取了数据库数据后做拼接操作,比如两张用户表:

1
2
复制代码user_info:{id,user_no,user_name,user_password}
user_other_info:{id,user_no,idcard,realname}

拼接后mongo数据为:

1
复制代码user:{_id,user_no,userInfo:{id,user_no,user_name,user_password},userOtherInfo:{id,user_no,idcard,realname})

接收到的数据信息很多,如何才能简单的触发数据拼接操作呢?

先看我们能获取的信息:schemaName,tableName,DBObject,Event(insert,update,delete)

将这些信息标识拼接起来看看:/schemaName/tableName/Event(DBObject),没错,就是一个标准的restful链接。只要我们实现一个简单的springMVC 就能自动获取需要的数据信息进行拼接操作。

先实现@Controller,定义名称为Schema,value对应schemaName

1
2
3
4
5
6
7
8
9
> 复制代码@Target({ElementType.TYPE})
> @Retention(RetentionPolicy.RUNTIME)
> @Documented
> @Component
> public @interface Schema {
> String value() default "";
> }
>
>

然后实现@RequestMapping,定义名称为Table,直接使用Canal中的EventType 对应RequestMethod

1
2
3
4
5
6
7
8
9
> 复制代码@Target({ElementType.METHOD, ElementType.TYPE})
> @Retention(RetentionPolicy.RUNTIME)
> @Documented
> public @interface Table {
> String value() default "";
> CanalEntry.EventType[] event() default {};
> }
>
>

然后创建springUtil,实现接口ApplicationContextAware,应用启动 加载的时候初始化两个Map:intanceMap,handlerMap

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
> 复制代码private static ApplicationContext applicationContext = null;
> //库名和数据处理Bean映射Map
> private static Map<String, Object> instanceMap = new HashMap<String, Object>();
> //路劲和数据处理Method映射Map
> private static Map<String, Method> handlerMap = new HashMap<String, Method>();
> @Override
> public void setApplicationContext(ApplicationContext applicationContext) {
> if (SpringUtil.applicationContext == null) {
> SpringUtil.applicationContext = applicationContext;
> //初始化instanceMap数据
> instanceMap();
> //初始化handlerMap数据
> handlerMap();
> }
> }
> private void instanceMap() {
> Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Schema.class);
> for (Object bean : beans.values()) {
> Class<?> clazz = bean.getClass();
> Object instance = applicationContext.getBean(clazz);
> Schema schema = clazz.getAnnotation(Schema.class);
> String key = schema.value();
> instanceMap.put(key, instance);
> logger.info("instanceMap [{}:{}]", key, bean == null ? "null" : clazz.getName());
> }
> }
> private void handlerMap() {
> if (instanceMap.size() <= 0)
> return;
> for (Map.Entry<String, Object> entry : instanceMap.entrySet()) {
> if (entry.getValue().getClass().isAnnotationPresent(Schema.class)) {
> Schema schema = entry.getValue().getClass().getAnnotation(Schema.class);
> String schemeName = schema.value();
> Method[] methods = entry.getValue().getClass().getMethods();
> for (Method method : methods) {
> if (method.isAnnotationPresent(Table.class)) {
> Table table = method.getAnnotation(Table.class);
> String tName = table.value();
> CanalEntry.EventType[] events = table.event();
> //未标明数据事件类型的方法不做映射
> if (events.length < 1) {
> continue;
> }
> //同一个方法可以映射多张表
> for (int i = 0; i < events.length; i++) {
> String path = "/" + schemeName + "/" + tName + "/" + events[i].getNumber();
> handlerMap.put(path, method);
> logger.info("handlerMap [{}:{}]", path, method.getName());
> }
> } else {
> continue;
> }
> }
> } else {
> continue;
> }
> }
> }
>
>

调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> 复制代码public static void doEvent(String path, DBObject obj) throws Exception {
> String[] pathArray = path.split("/");
> if (pathArray.length != 4) {
> logger.info("path 格式不正确:{}", path);
> return;
> }
> Method method = handlerMap.get(path);
> Object schema = instanceMap.get(pathArray[1]);
> //查找不到映射Bean和Method不做处理
> if (method == null || schema == null) {
> return;
> }
> try {
> long begin = System.currentTimeMillis();
> logger.info("integrate data:{},{}", path, obj);
> method.invoke(schema, new Object[]{obj});
> logger.info("integrate data consume: {}ms:", System.currentTimeMillis() - begin);
> } catch (Exception e) {
> logger.error("调用组合逻辑异常", e);
> throw new Exception(e.getCause());
> }
> }
>
>

数据拼接消息处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> 复制代码@Schema("demo_user")
> public class UserService {
> @Table(value = "user_info", event = {CanalEntry.EventType.INSERT, CanalEntry.EventType.UPDATE})
> public void saveUser_UserInfo(DBObject userInfo) {
> String userNo = userInfo.get("user_no") == null ? null : userInfo.get("user_no").toString();
> DBCollection collection = completeMongoTemplate.getCollection("user");
> DBObject queryObject = new BasicDBObject("user_no", userNo);
> DBObject user = collection.findOne(queryObject);
> if (user == null) {
> user = new BasicDBObject();
> user.put("user_no", userNo);
> user.put("userInfo", userInfo);
> collection.insert(user);
> } else {
> DBObject updateObj = new BasicDBObject("userInfo", userInfo);
> DBObject update = new BasicDBObject("$set", updateObj);
> collection.update(queryObject, update);
> }
> }
> }
>
>

示例源码

github.com/zhangtr/can…

欢迎讨论方案或者指正代码

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RCurl中这么多get函数,是不是一直傻傻分不清!!!

发表于 2017-11-20

你想知道R语言中的RCurl包中一共有几个get开头的函数嘛,今天我特意数了一下,大约有十四五个那么多(保守估计)!

所以如果对这个包了解不太深入的话,遇到复杂的数据爬取需求,自然是摸不着头脑,心碎一地_

实际上很多我们都不常用,常用的不超过五个,而且这些函数命名都很有规律,一般是类似功能的名称中都有统一的关键词标识,只要理解这些关键词,很好区分,下面我对9个可能用到的get函数简要做一个分类。

第一类是get请求函数(参数直接写在URL里面)

1
2
3
4
5
复制代码getURL              #get请求的一般形式
getBinaryURL #get请求二进制资源
getURLContent #get请求(可以根据返回状态的ContentType决定返回内容是文本格式还是二进制格式,
#所以说它其实就是前两个函数的结合体,可以根据返回内容类型做智能判断)
getURIAsynchronous #这个函数文档给的解释是可以实现请求的异步发送和多并发,需要计算机的cpu支持多核性能,至今尚未尝试过!

以下两个也是get请求函数(参数可以写在单独的查询参数中)

1
2
复制代码getForm        #单独提交查询参数的get请求函数
getFormParams 可以根据带参数的URL,分解出原始参数对

容错与配置句柄函数

1
2
3
复制代码getCurlErrorClassNames  #排错函数,可以根据请求错误信息得到错误类型,方便后期排错
getCurlHandle #curl句柄函数(是请求回话维持与进程管理的最重要部分,所有登录操作、身份认证都都需要该函数的支持)
getCurlInfo #根据curl句柄的记录信息,返回各项目信息详情

接下来我们逐个尝试一遍上述函数的用法。

getURL

getURL函数是一个基础get请求函数,其核心参数主要有URL、.opt、curl、.encoding。

  • URL就是请求的对应网址链接。
  • curl参数是一个句柄函数,它的参数指定对象是一个内嵌函数,通常是curl = getCurlHandle(),getCurlHandle()函数内同样是配置信息,不过curl句柄函数内的所有配置信息是可以提供给全局使用的,多次携带,维持整个回话状态,相对于一组初始化参数,而.opt参数内的各项配置信息是当前get请求使用的,它会覆盖和修改curl句柄函数内的初始化信息(当没有提供.opt参数时,get请求仍然使用curl中的初始化参数。)
  • .opt是一个配置参数,它就收一组带有命名的list参数,这些通常包括httpheader、proxy、timeout、verbose、cookiefile(cookiejar)等配置信息。
  • .encoding是字符集编码,这个通常可以通过请求的相应头ContType获取。

使用getURL发送一个完整的请求一般形式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码library("RCurl")
library("XML")
debugInfo <- debugGatherer() #错误信息收集函数
headers<-c("User-Agent" = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36")
###curl句柄函数
handle <- getCurlHandle(
debugfunction = debugInfo$update,
followlocation = TRUE,
cookiefile = "",
verbose = T
)

url<- "https://edu.hellobi.com/"
response<-getURL(
url, ###URL地址
#局部配置参数(作用于本次请求)
.opts=list(header=TRUE,httpheader = headers),
curl=handle, ###curl句柄,初始化配置参数(.opts内的声明的配置参数会覆盖curl中的默认参数)
.encoding="utf-8" ###编码参数
)


请求成功!

以上是getURL的一般形式,当然实际使用时,可以酌情调整参数,通常情况下,无需维持回话的话,curl不需要自己构造,函数会默认帮我们构造以个curl句柄。但是.opts参数使我们在当前请求中实际应用的配置参数信息,需要特别注意。

1
2
3
复制代码cat(debugInfo$value()[1])  #服务器地址及端口号
cat(debugInfo$value()[2]) #服务器返回的相应头信息
cat(debugInfo$value()[3]) #返回的请求头信息

))
debugGatherer函数收集的请求与相应信息对于后期的错误判断与bug修复很有价值!

getBinaryURL

二进制资源一般是指网络服务器上的二进制文件、图像文件、音视频等多媒体文件。这些资源通常可以直接通过download函数进行请求下载,但是getBinaryURL函数可以添加更多配置信息,在 请求资源是更加安全。

1
2
3
复制代码url<-"https://pic3.zhimg.com/720845d4f960c680039dbf7cc83ec21a_r.jpg"
response<-getBinaryURL(url)
writeBin(response,"720845d4f960c680039dbf7cc83ec21a_r.jpg")


下载到本地之后,打开正常!


你可以使用%>%管道函数把两句封装在一起,使用起来非常方便,比自带的download函数代码参数还少。除了图片之外,csv文件、xlsx文件、pdf文件、音视频文件都可以下载。

1
2
3
4
5
复制代码response<-getURLContent("https://pic3.zhimg.com/720845d4f960c680039dbf7cc83ec21a_r.jpg")
response<-getURLContent("https://edu.hellobi.com/")
class(response)
[1] "raw"
[1] "character"

使用getURLContent请求网页时,返回的是字符串(未解析的HTML文档),请求图片时,反回的是bytes值。不那么讲究的场合,getURLContent可以替代getURL或者getBinaryURL,但是通常为了便于记忆,一般请求网页使用getURL,请求二进制文件使用getBinaryURL,实际上三个函数仅仅是返回值的差异,通过参数设置的转换,基本可以相互替代。

getURIAsynchronous函数运行执行多并发任务,具有异步请求的功能,但是这一块我还没有研究透彻,至今尚未涉足,感兴趣的小伙伴儿可以自己试一试,将请求URL作为一个多值向量,闯进去就可以了,勇于探索才能学到好玩的东西。

getForm

getForm发送单独携带查询参数的get请求,这在之前的趣直播数据抓取中已经演示过了。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码library("magrittr")
url<-"http://m.quzhiboapp.com/api/lives/listOrderByPlanTs"
header=c(
"Accept"="application/json, text/plain, */*",
"User-Agent"="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36"
)
debugInfo <- debugGatherer()
handle<-getCurlHandle(debugfunction=debugInfo$update,followlocation=TRUE,cookiefile="",verbose = TRUE)
content<-getForm(url,.opts=list(httpheader=header),.params=list("limit"=30),.encoding="utf-8",curl=handle) %>% jsonlite::fromJSON()
###请注意这里getForm函数与getURL函数的区别
###(多了一个.params参数,它就是用于存放get请求的参数的,getFrom可以提供专门的查询参数)
head(content %>% `[[`(2))


getFormParams

getFormParams函数可以还原URL中的查询参数。

1
2
复制代码url<-"https://www.baidu.com/s?wd=writeBin&rsv_spt=1&rsv_iqid=0xe52332670003a3de&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&rqlang=cn&tn=baiduhome_pg&rsv_enter=1&rsv_t=222fgx%2FjxyaSTATqTxa8ljEv1u8i5TvyENqvcB1Ku1QcP3ZUR2pqIQ2sntZFOTlA4NJx&oq=writeBin%2520%25E4%25B8%258B%25E8%25BD%25BD%25E5%259B%25BE%25E7%2589%2587&inputT=1300&rsv_sug3=13&rsv_pq=e9db342c0003c543&rsv_sug1=6&rsv_sug7=100&rsv_sug2=0&rsv_sug4=1940&rsv_sug=1"
getFormParams(url)


还原结果是一个带有命名的字符串向量。

getCurlErrorClassNames 函数是一个排错函数,具体怎么用我也不知道,目前还没有用过,感兴趣的自己探索!


getCurlHandle\getCurlInfo

getCurlHandle 函数是全局的curl句柄函数,包含所有请求、相应以及本地终端与web服务器之间的通讯记录。它用于构建初始化配置函数。它通常与getCurlInfo 搭配使用。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码debugInfo <- debugGatherer()    
#错误信息收集函数
headers<-c("User-Agent" = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36")
handle <- getCurlHandle(
debugfunction = debugInfo$update,
followlocation = TRUE,
cookiefile = "",
verbose = T
)

url<- "https://edu.hellobi.com/"
response<-getURL(url,.opts=list(header=TRUE,httpheader = headers),curl=handle,.encoding="utf-8")

比如可以通过getCurlInfo 函数获取handle中的所有信息。

1
2
3
4
5
6
7
复制代码getCurlInfo(handle) %>% names()
cat(getCurlInfo(handle)$effective.url)
https://edu.hellobi.com/
cat(getCurlInfo(handle)$response.code)200cat(getCurlInfo(handle)$content.type)
text/html; charset=UTF-8
cat(getCurlInfo(handle)$cookielist)
.hellobi.com TRUE / FALSE 1511007061 XSRF-TOKEN eyJpdiI6IkRLcndIcW0raVF3aDNkbjJRRVJwTHc9PSIsInZhbHVlIjoiRkFDMWdCdEJ4dGFOYTJBaWV2c3pHZm1IOHZoOW81eisxaFJENzJSRnVGXC9TQnZKTDRjZFQ3NlpicnVoODF3N0U3VTZuNXJHREtYbDR5SDc5YkREMGVnPT0iLCJtYWMiOiIzNWI2OTYyYWNlZmQwMDc1N2Q1Y2I5NTY4NzUxZWIzYWYwZDM0N2MzZmExMzQ1OGJlNDVjNmZiMzEwMWY4MjEwIn0%3D #HttpOnly_.hellobi.com TRUE / FALSE 1511007061 laravel_session eyJpdiI6Ik8zN3NUMjFjN2FRa1dldXZ4REhVTnc9PSIsInZhbHVlIjoiQ29kakpTRjFGdno5c2Y0elpSZWNhVFVpckJWTnRwaENaeHgxSThmOXRDNDU0T0k1djlUV011ZlBEQXV5N2drc3NBOHBzY0FWZEJSRG1ocjNweGpXNnc9PSIsIm1hYyI6IjZjOWRlMmNjMjg3MGM3MTEzZDA0N2M2OTdkNGUwYWM3MzY0N2MwNmJmYmMxZDAzYTllODEzZjQ0YWUzNjA4NjQifQ%3D%3D


好了,到这里,RCurl的几个重要get函数几乎都已经讲完了,接下来会抽时间整理一下RCurl的中postForm函数的四种常见参数提交方式,以及curl句柄函数配置参数的权限类型,RCurl这个包经过这些时间的梳理,已经扒的差不多了,以后若是时间允许,可以探索一下RCurl中的并发与异步请求实现方式。

其实除了RCurl之外,rvest包也有很多好玩的东西,最近的探索发现,rvest本身并不神奇,它作为一个底层请求器httr以及解析器selectr包、xml2包的封装,整合了这些包的优点,在解析方面大有可为,但是请求功能上很薄弱,它的css解析器实现其实是在内部调用selectr包中的css_to_xpath函数,将css语法转化为xpath之后才开始解析的,这样如果你能花些时间学一下xml2\httr\selectr的话,几乎可以完全绕过rvest包,自己灵活构建请求与解析函数了,这三个包文档都很少(httr稍多一些!)。

还计划想写一篇关于R爬虫与Python对比的文章,R语言与Python在很多领域一直相爱相杀,Python的DataFrame貌似参考了R里面的data.frame,并且移至了R语言中的ggplot2,而R语言中,哈德利写的xml2包是由BeautifulSoup激发的的灵感,rvest包的初衷参照requests的框架,以后没事儿多八卦一些R语言与Python背后的故事,感觉蛮好玩的!

在线课程请点击文末原文链接:

Hellobi Live | 9月12日 R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Da…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

扒一扒rvest的前世今生!

发表于 2017-11-20

rvest包可能是R语言中数据抓取使用频率最高的包了,它的知名度和曝光度在知乎的数据分析相关帖子和回答中都很高。

甚至很多爬虫教程和数据分析课程在讲解R语言网络数据抓取时,也大多以该包为主。

坦白的说,rvest的确是一个很好地数据抓取工具,不过他的强项更多在于网页解析,这一点儿之前就有说到。

你可能惊艳于rvest强大的解析能力,有两套解析语法可选(Xpath、css),短短几个关键词路径就可以提取出来很重要的数据。

但肯定也遇到过有些网页明明数据就摆在那里,通过Chrome开发者工具(或者selectorgadget)也copy了css或者xpath路径,可就是没有返回值,或者总是返回chracter(0)、list(0)或者NULL。

老实说,这个情况真的不能怪rvest,这与rvest的功能定位有关。这里我们看一下rvest的GitHub主页上hadley对rvest的定位:

rvest helps you scrape information from web pages. It is designed to work with magrittr to make it easy to express common web scraping tasks, inspired by libraries like beautiful soup.


rvest旨在帮助我们从网页获取信息,通过植入magrittr的管道函数使得常见的网络抓取任务更加便捷,它的灵感来源于BeautifulSoup(注:这是一个Python非常有名并且强大的网页解析库)。

以下是我的个人愚见,这里的网络抓取存在一个前提,即你有权限直接通过URL获取完整网页(注意是完整网页)或者,你已经通过其他请求库(比如RCurl或者httr)获取了完整的网页,那么剩余的事情就交给rvest吧,它一定帮你办的妥妥的(前提css和xpath要熟练)。

当然rvest包允许你直接通过url访问html文档,但是这种访问方式是很脆弱的,因为没有任何伪装措施和报头信息,直接访问存在着很大的隐患。还记得之前讲异步加载的时候说过的,ajax技术将请求的的网络资源分成了html纯文档和js脚本,浏览器可以通过解析并执行js脚本来更新关键数据,而通过其他非浏览器终端发送的请求,通常情况下只能拿到纯文档(你可以看到一些script标签中引用的的.js脚本),并不具备解析js脚本的能力。

接下来扒一扒rvest包中主要函数的源码,给我以上的观点多一些充足的论据!

1
2
3
4
复制代码library("rvest")
library("magrittr")
library("xml2")
library("selectr")

rvest包的几个重要函数:

1
2
3
4
5
6
复制代码read_html()
html_nodes()
html_attrs()
html_text()
html_table()
htmm_session()

相信对于rvest包而言,你常用的函数不无外乎这几个,接下来我们对照着这几个函数的源码,一个一个剖析!read_html

1
2
3
4
5
6
复制代码function (x, encoding = "", ..., options = c("RECOVER", "NOERROR", 
"NOBLANKS"))
{
UseMethod("read_html")
}
<environment: namespace:xml2>

你会发现,read_html函数直接调用的是xml2包中的read_html方法,rvest包作为请求器的脆弱性便在于此,它是一个I/0函数。脱俗一点说就是文件导入导出的操纵函数,与read_csv、read_xlsx、read_table属于同类。

在XML包中与之功能一致的函数是xmlParse/xmlTreeParse。xmlParse/xmlTreeParse函数也是仅仅作为RCurl请求包的解析函数使用的,很少有单独使用xmlParse请求并解析网页(太脆弱了,尽管它是支持直接从url获取并解析网页的)。

当然,这并不妨碍rvest包(read_html函数)直接从某些网站的URL中解析数据,很多静态网页并不会对网络请求做过多限制,比如不检查User-Agent,不做任何的数据隐藏,不限制数据权限等。

对于获取并解析网页而言,你可以直接加载xml2包,使用其read_html函数。

html_nodes

html_nodes函数可能是rvest包中封装的最为成功的函数了,就是这个函数提供给大家两套网页解析语法:xpath、css。看下它的源码吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码html_nodes <- function(x, css, xpath) {
UseMethod("html_nodes")
}
#' @export

html_nodes.default <- function(x, css, xpath) {
xml2::xml_find_all(x, make_selector(css, xpath))
}

make_selector <- function(css, xpath) {
if (missing(css) && missing(xpath))
stop("Please supply one of css or xpath", call. = FALSE)
if (!missing(css) && !missing(xpath))
stop("Please supply css or xpath, not both", call. = FALSE)
if (!missing(css)) {
if (!is.character(css) && length(css) == 1)
stop("`css` must be a string")
selectr::css_to_xpath(css, prefix = ".//")
} else {
if (!is.character(xpath) && length(xpath) == 1)
stop("`xpath` must be a string")
xpath
}
}

接下来给大家剖析这段html_nodes的源码,首先定义了一个基于S3类的泛型函数——html_nodes。这个泛型函数的模型行为是html_nodes.default。

html_nodes.default函数中,使用的是xml2包中的xml_find_all函数,这才是rvest包强大解析能力的核心底层实现。无论你传入的是css路径还是xpath路径,最终都是通过这个函数实现的。

xml_find_all函数中又使用了一个make_selector函数,他是一个选择器,即在css路径表达式和xpath选择。make_selector函数首先判断提供的解析语法参数是否完备,当你没有提供任何一个解析语法的时候(html_nodes()函数中除了doc文档之外,没有提供xpath或者css备选参数),抛出错误并中断操作:Please supply one of css or xpath,当同时提供了css和xpath参数时也会抛出错误并中断执行,Please supply
css or xpath, not both。

当你提供css参数时(因为这里是位置参数,所以除了 第一个参数是html文档之外,只提供一个未命名参数会被当做css参数处理,想要使用xpath参数必须显式声明——xpath=”path”)。函数会判断css参数是否合法,不合法则会报错,合法之后,会使用selectr包中的css_to_xpath函数将css路径表达式转换为xpath语法,然后输出,当你提供的是xptah路径时(需需显式声明参数名称),首先校验xpath是否合法,不合法则报错,合法则返回xptah路径。

所以以上的核心要点有两个:

  • 在html_nodes函数中,最终的解析函数是xml2中的xml_find_all函数,它的功能类似于XML包中的XpathAapply函数或者getNodest函数。
  • 在html_nodes函数中,一切都是xpath,即便你提供的是css路径,也会先被转化为xpath之后再使用xml_find_all函数进行处理。

html_attrs

好了接下来该讲html_attrs函数了,还是看源码:

1
2
3
4
5
复制代码function (x) 
{
xml2::xml_attrs(x)
}
<environment: namespace:rvest>

仍然是,直接调用的xml2包中的xml_attrs函数,就是从节点中批量提取属性值。

html_text

1
2
3
4
5
复制代码function (x, trim = FALSE) 
{
xml2::xml_text(x, trim = trim)
}
<environment: namespace:rvest>

调用的xml2包中的xml_text函数,提取节点文本。

html_table

1
2
3
4
5
复制代码function (x, header = NA, trim = TRUE, fill = FALSE, dec = ".") 
{
UseMethod("html_table")
}
<environment: namespace:rvest>

html_table函数是做了一些高级的封装,但是底层仍然时通过xml2::xml_find_all实现的,它将table标签提取出来之后,又做了一些清洗整理。

源码在这里:
github.com/hadley/rves…

html_session

htmm_session可以实现一些简单的回话维持和cookie管理功能,但是该包的源文档并没有给出任何实际案例,网络上类似资料也极少。它的底层是通过封装httr包中的handle函数来实现的,这算是rvest包的较为高级功能了,里面确实封装了一些真正的GET请求、POST请求构造类型。但是平时能用到的人估计不多。我看了下源码,回头乖乖去看httr文档去了。

源码在这里:

github.com/hadley/rves…

至此,主要的rvest函数都撸完一个遍了,这里给rvest做一个小结吧:

它的高级请求功能依托于httr(当然你可以直接使用httr来构造请求)。
解析器依托于xml2包中的xml_find_all函数实现。
解析语法有css和xpath可选,但是最终都会转换为xpath进行解析。
借助magrittr包来做管道优化,实现代码简化与效率提升。

如果要跟Rcurl和XML组合来一个对比,我觉得这里可以这么比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码########################################################################################################
包类型 RCurl httr
--------------------------------------------------------------------------------------------------------
请求 GET请求 getURL/getFrom GET
getBinaryURL/writeBin(二进制) writeBin(二进制)
POST请求 postFrom(支持四种常规参数编码类型) POST(支持四种常规参数编码类型)
application/x-www-form-urlencoded
multipart/form-data
application/json
text/xml
--------------------------------------------------------------------------------------------------------
解析(html/xml) XML::xmlParse/XML::htmlParse rvest::read_html/xml
xml2::read_html/xml
--------------------------------------------------------------------------------------------
Xpath XML::xpathSApply/XML::getNodeSet rvest::html_nodes/xml2::xml_find_all
XML::xmlGetAttr rvest::html_attrs/xml2::xml_attrs
XML::xmlValue rvest::html_text/xml2::xml_text
---------------------------------------------------------------------------------------------
CSS selectr::querySelectorAll selectr::querySelectorAll
---------------------------------------------------------------------------------------------------------
高级回话管理 curl<-getCurlHandle curl<-handle()
---------------------------------------------------------------------------------------------------------
json返回值 jsonlite::fromJSON jsonlite::fromJSON
#########################################################################################################

如果你想了解更多关于css路径表达式,xpath解析的相关知识,可以去W3c学习全套的技术标准,也可以参考以下这几篇文章:

左手用R右手Python系列16——XPath与网页解析库

左手用R右手Python系列17——CSS表达式与网页解析

R语言数据抓取实战——RCurl+XML组合与XPath解析

左手用R右手Python系列——模拟登陆教务系统

Python网络数据抓取实战——Xpath解析豆瓣书评

左手用R右手Python——CSS网页解析实战

左手用R右手Python系列——模拟登陆教务系统

如果想了解抓包流程和json返回值处理,可以参考以下几篇内容:

网易云课堂Excel课程爬虫思路

左手用R右手Pyhon系列——趣直播课程抓取实战

Python数据抓取与可视化实战——网易云课堂人工智能与大数据板块课程实战

R语言网络数据抓取的又一个难题,终于攻破了!

R语言爬虫实战——网易云课堂数据分析课程板块数据爬取

R语言爬虫实战——知乎live课程数据爬取实战

以上便是,你可以在整个R语言的爬虫相关工具体系中找到rvest的位置,他是xml2包的进一步高级封装和替代。

至于浏览器驱动的网络请求,在R语言中,有Rwebdriver包和Rselenium包可以支持,并且支持大部分主流浏览器(IE、Chrome、Firfox、PlantomJS)。

当你看到这个R语言爬虫工具列表时,你肯定会很惊讶,哇塞,原来R语言的爬虫功能这么强大耶,的确如此,太多的高级功能只是无人问津罢了。

R语言缺的就是没有像Python中那么强大的可以构建工程项目用的框架,比如Scrapy这种的。

在线课程请点击文末原文链接:

Hellobi Live | 9月12日 R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Da…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

对象池的一个 race condition

发表于 2017-11-20

拙作《Linux 多线程服务端编程》第 1.11 节介绍了如何用 shared_ptr/weak_ptr 实现对象池,最近有读者指出对象销毁有 race condition。本文介绍一下复现及修复的方法。

从第 22 页的 version 3 开始的代码有这个 race condition,包括第 1.11.1 节的 version 4 和第 1.11.2 节的弱回调版,见试读样张,配套代码见 GitHub。这个
race condition 再次验证了对象的销毁比创建更难。

Race condition

为了突出重点,本文以 version 3 为例,介绍 race condition 的成因及修复方法,完整代码(包括修复)见 GitHub。为了便于下文讨论,我把 version 3 代码的代码用 C++11 重新实现,贴在这里。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码class Stock : boost::noncopyable
{
public:
Stock(const string& name)
: name_(name)
{
}

const string& key() const { return name_; }

private:
string name_;
};

// 对象池
class StockFactory : boost::noncopyable
{
public:

std::shared_ptr<Stock> get(const string& key)
{
std::shared_ptr<Stock> pStock;
muduo::MutexLockGuard lock(mutex_);
std::weak_ptr<Stock>& wkStock = stocks_[key];
pStock = wkStock.lock();
if (!pStock)
{
pStock.reset(new Stock(key),
[this] (Stock* stock) { deleteStock(stock); });
wkStock = pStock;
}
return pStock;
}

private:

void deleteStock(Stock* stock)
{
if (stock)
{
muduo::MutexLockGuard lock(mutex_);
stocks_.erase(stock->key());
}
delete stock;
}

mutable muduo::MutexLock mutex_;
std::unordered_map<string, std::weak_ptr<Stock> > stocks_;
};

Race condition 发生在 StockFactory::deleteStock() 这个成员函数里,如果进入 deleteStock 之后,在 lock 之前,有别的线程调用了相同 key 的 StockFactory::get(),会造成此 key 被从 stocks_ 哈希表中错误地删除,因此会重复创建 Stock 对象。程序不会 crash 也不会有 memory leak,但是程序中存在两个相同 key 的 Stock 对象,违背了对象池应有的语意。下图描绘了 race
condition 的发生过程。


复现
–

这个 race condition 可以用 sleep() 很容易地复现出来,见 GitHub 上的代码,编译时须定义 REPRODUCE_BUG 这个宏。

修复

修复这个 race condition 的办法很简单,在 deleteStock() 中,拿到 lock 之后,检查一下 weak_ptr 是否 expired(),然后只在 expired() 为 true 的情况下从 stocks_ 中删掉 key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码void deleteStock(Stock* stock)
{
if (stock)
{
muduo::MutexLockGuard lock(mutex_);
auto it = stocks_.find(stock->key());
assert(it != stocks_.end());
if (it->second.expired())
{
stocks_.erase(it);
}
}
delete stock;
}

修复之后,原来的 race condition 不复存在:


思考题


如果把条件 if (it->second.expired()) 改成 if (!it->second.lock()),即试着将 weak_ptr 提升为 shared_ptr,如果提升不成功,则 erase key,这样做有没有问题?

这样做有可能造成死锁,因为 muduo Mutex 是不可重入的。race condition:如果 weak_ptr::lock() 成功,拿到一个 shared_ptr (use_count 应该 > 1),然后在此 shared_ptr 析构之前,其他线程释放了这个对象,使得 use_count
降为 1,那么当此 shared_ptr 析构的时候,会递归调用 deleteStock(),从而造成死锁。


题图
–

Herb Sutter 在 CppCon2016 上也提到了类似的对象池技术,他的实现对应书中的 version 2,没有这个 race condition,但对象池的大小只增不减。演讲视频:My CppCon talk video is online,幻灯片:
CppCon/CppCon2016。

题外话

承蒙读者厚爱,《Linux 多线程服务端编程》自从 2013 年 1 月面世以来,截至 2017 年 11 月,累计印刷 10 次,印数共 2 万册。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入理解JVM类文件格式

发表于 2017-11-20

我们知道Java最有名的宣传口号就是:“一次编写,到处运行(Write Once,Run Anywhere)”,而其平台无关性则是依赖于JVM, 所有的java文件都被编译成字节码(class)文件,而虚拟机只需要认识字节码文件就可以了。想要弄懂虚拟机以及类加载机制,这部分内容是不可不知的。

JVM

JVM

Class文件是一组以8字节为基础单位的二进制流,所有数据无间隔的排列在Class文件之中,多字节数据以大端(big-endian order)的方式存储。Class文件以一种接近于C中结构体的伪代码形式存储数据结构,并且只包含无符号数和表两种数据结构:

  • 无符号数:u1、u2、u4、u8分别表1、2、4、8字节的无符号数
  • 表: 由多个无符号数或者其他表组成的复合数据类型, Class文件本身也是一张表。

Class表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

参照上面的数据结构,Class文件由10个部分组成:
1 . 魔数
2 . Class文件主次版本号
3 . 常量池
4 . 访问标记
5 . 当前类名
6 . 父类名
7 . 继承的接口名
8 . 包含的所有字段的数量+字段
9 . 包含的所有方法的数量+方法
10 . 包含的所有属性的数量+属性

下面我们依次对每个部分进行分析:

1. 魔数

魔数(Magic number)用来确定文件类型,这里就是检测文件是否是能够被虚拟机接受的Class文件。很多文件都使用魔数来确定文件类型,而不是扩展名(因为扩展名可以任意修改)。可以参看我的深入理解程序构造(一)。

Class文件的魔数是“0xcafebabe”,咖啡宝贝?Java本身也是一种爪哇咖啡,真是挺有缘的。
这里我也写个小的测试程序,来看看它的二进制码流:

1
2
3
4
5
6
7
8
复制代码package com.shuqing28;

public class TestClass {
private int m;
public int inc() {
return m+1;
}
}

我们使用javac编译成.class文件,Windows下可以使用WinHex打开,Linux下则可以使用hexdump打开二进制,命令如下:

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
26
复制代码$ hexdump -C TestClass.class 
00000000 ca fe ba be 00 00 00 34 00 16 0a 00 04 00 12 09 |.......4........|
00000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 |.............m..|
00000020 01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 |.I...<init>...()|
00000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e |V...Code...LineN|
00000040 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 |umberTable...Loc|
00000050 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 |alVariableTable.|
00000060 00 04 74 68 69 73 01 00 19 4c 63 6f 6d 2f 73 68 |..this...Lcom/sh|
00000070 75 71 69 6e 67 32 38 2f 54 65 73 74 43 6c 61 73 |uqing28/TestClas|
00000080 73 3b 01 00 03 69 6e 63 01 00 03 28 29 49 01 00 |s;...inc...()I..|
00000090 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 0e 54 65 |.SourceFile...Te|
000000a0 73 74 43 6c 61 73 73 2e 6a 61 76 61 0c 00 07 00 |stClass.java....|
000000b0 08 0c 00 05 00 06 01 00 17 63 6f 6d 2f 73 68 75 |.........com/shu|
000000c0 71 69 6e 67 32 38 2f 54 65 73 74 43 6c 61 73 73 |qing28/TestClass|
000000d0 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a |...java/lang/Obj|
000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|
000000f0 05 00 06 00 00 00 02 00 01 00 07 00 08 00 01 00 |................|
00000100 09 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 |..../........*..|
00000110 01 b1 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 |................|
00000120 00 03 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c |................|
00000130 00 0d 00 00 00 01 00 0e 00 0f 00 01 00 09 00 00 |................|
00000140 00 31 00 02 00 01 00 00 00 07 2a b4 00 02 04 60 |.1........*....`|
00000150 ac 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 |................|
00000160 06 00 0b 00 00 00 0c 00 01 00 00 00 07 00 0c 00 |................|
00000170 0d 00 00 00 01 00 10 00 00 00 02 00 11 |.............|
0000017d

看第一行的前4个字节的十六进制就是0xcafebabe,所以文件类型确实为.class文件。

2. 版本号

第5和第6字节是次版本号(Minor Version),第7和第8字节是主版本号(Major Version)。这里看出我们的主版本号是0x0034,也就是52,下面是JDK与其对应的版本号关系:

JDK 1.8 = 52
JDK 1.7 = 51
JDK 1.6 =50
JDK 1.5 = 49
JDK 1.4 = 48
JDK 1.3 = 47
JDK 1.2 = 46
JDK 1.1 = 45

可以看出我使用的是Java8编译的代码。

3. 常量池

我们继续看二进制文件的第一行:

1
复制代码00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|

在主版本号0x0034后的是0x0016,这个值表示常量池的容量。常量池可以理解为Class文件的资源仓库,常量池中包含的数据结构是这样的:

1
2
3
4
复制代码cp_info {
u1 tag;
u1 info[];
}

常量池中的每个项目都包含一个tag开头的cp_info对象,代表着常量类型,info则根据不同的类型各有各的结构。目前一共有14种常量类型:

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

上面的0x0016翻译成十进制是22,那么常量池中有21个常量,因为常量池中索引是从1开始计数的,所以常量索引范围是1~21。

1
复制代码00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|

接下看常量池的第一个常量, tag是0x0a, 查上面的常量表就是CONSTANT_Methodref,表示接下来定义的是一个方法,知道类型后,我们可以查一下CONSTANT_Methodref的结构,这里可以参考Oracle的官方文档The class File Format,

1
2
3
4
5
复制代码CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

由于.class文件是无间隔的二进制文件,所以接着读:

  • tag: 0x0a,上面已经说了指代CONSTANT_Methodref常量
  • class_index:指向常量池中CONSTANT_Class_info类型的常量,代表上面方法的名称
  • name_and_type_index : 指向常量池中CONSTANT_NameAndType_info常量,是对方法的描述

因为class_index占两个字节,所以紧接着读到了0x0004,也就是4,指向常量池中的第4个常量,name_and_type_index是0x0012,指向第18个常量。后面会分析到第4和第18个常量。

继续往下读,到第一行的最末了,是个0x09,指示的是CONSTANT_Fieldref,表示接下来是对一个域的定义, 查官方文档,格式为:

1
2
3
4
5
复制代码CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

结构和CONSTANT_Methodref_info一样,这时候读到了第二行:

1
复制代码00000010  00 03 00 13 07 00 14 07  00 15 01 00 01 6d 01 00  |.............m..|

class_index为0x0003,指向第3个常量,name_and_type_index为0x0013指向第13个常量。这时候继续往后读,终于读到第3个常量了。此时tag是0x07,查表可得为CONSTANT_Class类型,此类型的常量代表一个类或者接口的符号引用,CONSTANT_Class的结构:

1
2
3
4
复制代码CONSTANT_Class_info {
u1 tag;
u2 name_index;
}

tag是7, name_index是0x0014,十进制就是20,指向第20个常量,这样我们已经读了很多个字节了。但是这样解析下去很累,还好java自带的javap工具可以帮我们分析出字节码的内容。
执行下面语句:

1
复制代码javap -verbose TestClass.class

我们可以得到:

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
26
27
28
29
30
31
32
33
34
复制代码Last modified Nov 14, 2017; size 381 bytes
MD5 checksum 102d643185c4823ef103931ff3e34462
Compiled from "TestClass.java"
public class com.shuqing28.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/shuqing28/TestClass.m:I
#3 = Class #20 // com/shuqing28/TestClass
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/shuqing28/TestClass;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/shuqing28/TestClass
#21 = Utf8 java/lang/Object
{
public com.shuqing28.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
...//省略

这里我们可以看到Constant pool字段,后面依次列出了21个常量,可以看出第一个是Methodref型的常量,class_index指向第4个常量,第4个常量呢是CONSTANT_Class类型,name_index又指向第20个常量,可知是一个CONSTANT_Utf8类型的常量,前面没说到CONSTANT_Utf8,下面是它的结构:

1
2
3
4
5
复制代码CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

第一位tag为1,length指示字符数组的长度,bytes[length]是使用UTF-8缩略编码表示的字符串,这里解析出来是com/shuqing28/TestClass,即类的全限定名。

继续回到第一个Methodref常量,它的name_and_type_index值是18, 继续找到第18个常量,是CONSTANT_NameAndType_info类型,代表的是一个方法的信息:

1
2
3
4
5
复制代码CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}

name_index指向了常量7, 即#7 = Utf8 <init>, 是一个CONSTANT_Utf8_info类型,值为,这个是方法的名称,descriptor_index指向了常量8,即#8 = Utf8 ()V,是方法的描述,下文会说这个表达式是什么意思。
这样我们就可以一一把这21个常量分析清楚了。

其实Class文件就是在一开始列出了一堆常量,后面的各种描述都是各种index,指向前面常量池中的各种常量,来描述整个类的定义。就像有一本字典,我们使用字典中的字来造我们的句子,只不过Class文件中造句是有严格格式规定的,下面的内容基本都按照固定格式,无间隔的描述一个类的内容。

4. 访问标志

常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口的访问信息,包括这个Class是类还是接口,是否是public的,是否是abstract,是否是final的。
访问标记含义如下表:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

access_flags中一共有16个标志位可用,当前只定义了8个,别的都为0,TestClass是public类型的,且使用JDK1.2以后的编译器进行编译的(使用JDK1.2以后的编译器编译,这个值都为真),别的标志都为假。所以access_flags的值应为:0x0001|0x0020 = 0x0021。我们找到刚才常量池最后一行的地方:

1
复制代码000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|

65 63 74分别对应ect,紧接着是0x0021,与我们的分析结果一致。

5.类索引、父类索引与接口索引集合

引用文章开头的ClassFile的数据结构,这三项定义为:

1
2
3
4
复制代码    u2             this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];

类索引和父类索引都是u2类型的数据,而接口索引首先给出了接口的数量,然后才是一个包含接口的数组。这三个值揭示了一个类的继承关系。

1
复制代码000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|

接着前面的0x0021看,类索引为0x0003,指示常量池第3个常量,查上文可得#3 = Class #20 // com/shuqing28/TestClass,第3个常量又指向第20个常量,而第20个常量是一个CONSTANT_Utf8变量,其值为com/shuqing28/TestClass,表示类的全限定名字符串。
接下来的是0x0004是父类索引,指向常量池中第4个常量,即#4 = Class #21 // java/lang/Object, 又指向第21个变量,即java/lang/Object,我们知道Object是所有类的父类。
接下来的是0x0000,可见TestClass没有实现任何接口。

6.字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级别的变量以及实例级的变量,但是不包括方法内的局部变量。一个Java字段可以包括以下信息:字段的作用域、是实例变量还是类变量、是否是final、并发可见性(volatile),是否可以被序列化(transient)、字段数据类型。下面是字段表具体结构:

1
2
3
4
5
6
7
复制代码field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

再看access_flags可以取以下值:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; usable only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; never directly assigned to after object construction (JLS §17.5).
ACC_VOLATILE 0x0040 Declared volatile; cannot be cached.
ACC_TRANSIENT 0x0080 Declared transient; not written or read by a persistent object manager.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ENUM 0x4000 Declared as an element of an enum.

一般来说,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能存在一个,其它标志都按照Java语言本身的性质来。

在access_flags标志的后面是两项索引值name_index,descriptor_index,两个都是指向常量池的索引,分别代表字段的简单名称以及字段和方法的描述符。

这里我们梳理下简单名称、描述符以及全限定名这三个词对应的概念:
全限定名:前面提到的com/shuqing28/TestClass就是全限定名,它把java代码中所有的”.”替换成了”/“,一般使用”;”结尾。
简单名称:不带类型和修饰的方法或者字段名,上文中的代码里就是”inc”和”m”
至于方法描述符,描述的是数据类型、方法的参数列表和返回值。我们知道在C++中重载函数时函数实际上是换了名字的,包含了函数的参数,例如add(int x, int y),在编译后可能是Add_Int_Int, 但是在Java中我们把基本数据类型都用一个大写字符来表示,而对象类则是使用L+对象的全限定名来表示。

描述符标识字符含义:

标识字符 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L Object, 例如 Ljava/lang/Object

对于数组,前面加[就行,如java.lang.String[][],表达为[[java/lang/String, int[] 就被记录为[I。
用描述符描述方法时,按照参数列表,返回值的顺序描述,参数列表还需要放在括号内。比如前文提及的”() V” 就表示一个参数为空,返回值为void的方法,即代码中的void inc()方法。

举个复杂点的, int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex),其描述符为([CII[CIII) I。

继续分析我们前文中提及的程序的二进制代码:

1
2
复制代码000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|
000000f0 05 00 06 00 00 00 02 00 01 00 07 00 08 00 01 00 |................|

上一小节我们分析到第一行的0x0000了,接下来的是0x01,这个值其实代表了字段表的个数,我们的代码里只包含一个字段。接下来的是0x0002,这个字段是access_flags标志,查询后可知为ACC_PRIVATE,再接下来是0x0005, 从常量表清单上可以查到是#5 = Utf8 m, 再接着是descriptor_index, 其值为0x0006,查一下常量池为#6 = Utf8 I,可知这一句为private int m;

一般来说,在decriptor_index后,还有个属性集合用于存储一些额外信息,而0x0000代表没有属性字段。
如果把m字段声明为private static int m = 123; 则可能多一个ConstantValue属性,指向常量值123。

7.方法表集合

方法表集合和字段表集合非常相似,结构也是:

1
2
3
4
5
6
7
复制代码method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

只不过在访问标志和属性表集合的可选项有所不同。例如access_flags有以下可选值:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declaredprivate; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declaredprotected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declaredstatic.
ACC_FINAL 0x0010 Declaredfinal; must not be overridden
ACC_SYNCHRONIZED 0x0020 Declaredsynchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declarednative; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declaredabstract; no implementation is provided.
ACC_STRICT 0x0800 Declaredstrictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

可以看出,方法里增加了像ACC_SYNCHRONIZED,ACC_NATIVE,ACC_STRICT, ACC_ABSTRACT, 分别对应着synchronized、native、strictfp、abstract这些只能修饰方法的关键字。

现在我们就可以继续分析我们程序的二进制代码了。

1
2
复制代码000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|
00000100 09 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 |..../........*..|

上一小节我们刚刚分析到000000f0行的0x0000,接下来的是0x0002,代表有两个方法,接下来的几个字节是

  • 0x0001:访问标记是ACC_PUBLIC
  • 0x0007:名称索引指向第7个常量:
  • 0x0008:描述符索引指向第8个常量:()V
  • 0x0001:属性有一个
  • 0x0009:属性指向第9个常量,Code

我们正好有疑问,方法定义有了,方法体在哪呢,答案就是上面分析的最后一个Code。下一节就说说属性表集合的各种可能。

8.属性表集合

属性表(attribute_info)在前面已经多次提及,Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景转有的信息。
属性表并没有严格限制顺序,只要不与已有属性名重复,任何人实现的编译器都可以添加自己定义的属性信息,以下是一些预定义的属性:

属性名称 使用位置 含义
SourceFile ClassFile 记录源文件的名称
InnerClasses ClassFile 内部类列表
EnclosingMethod ClassFile 内部类才有这个属性,用于标识这个类所在的外围方法
SourceDebugExtension ClassFile 用于存储额外的调试信息,JDK1.6中新增
BootstrapMethods ClassFile 用于保存invokeddynamic指令引用的引导方法限定符,JDK1.7中新增
ConstantValue field_info final关键字定义的常量值
Code method_info Java代码编译成的字节码指令
Exceptions method_info 方法抛出的异常
RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations method_info 指明哪些参数是运行时可见的,哪些是运行时不可见的,JDK1.5中新增
AnnotationDefault method_info 记录注解类元素的默认值,JDK1.5中新增的
MethodParameters method_info 记录方法的参数信息,比如它们的名字,访问级别,JDK1.8新增
Synthetic ClassFile, field_info, method_info 表示方法或字段是编译器自动生成的
Deprecated ClassFile, field_info, method_info 被声明为deprecated的字段
Signature ClassFile, field_info, method_info 用于支持泛型情况下的方法签名,在Java语言中,如果任何类、接口、初始化方法或者成员的泛型签名包含了类型变量或者参数化类型,则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。JDK1.5中新增
RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations ClassFile, field_info, method_info 为动态注解提供支持,指明哪些是注解是运行时可见的,哪些是运行时不可见的,JDK1.5中新增
LineNumberTable Code Java源码的行号与字节码指令的对应关系
LocalVariableTable Code 方法的局部变量描述
LocalVariableTypeTable Code 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加,JDK1.5中新增
StackMapTable Code 供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作栈所需要的类型是否匹配,JDK1.6新增
RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations ClassFile, field_info, method_info, Code 记录运行时类型上注解的可见性,也包括运行时类型参数的注解的可见性

下面具体说一说一些比较重要的属性:

Code属性

首先来看Code属性的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
  • attribute_name_index: 占两个字节,指向CONSTANT_Utf8_info常量,表示属性名,这里固定是”Code”
  • attribute_length:属性值的长度,由于attribute_name_index和attribute_length占6个字节,所以attribute_length为属性表长度减6
  • max_statck: 操作数栈深度的最大值,在方法执行时,操作数栈不能超过这个值
  • max_locals: 局部变量所需的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配的最小单位,对于byte,char,float,int,short,boolean和returnAddress等长度不超过32位的数据类型,都只占一个slot,而double和long 这种64为的数据都是需要占用两个slot。方法参数(包括隐藏的this)、异常处理器的参数、方法体定义的局部变量都需要局部变量表来存放。但是max_locals并不是所有局部变量所占的slot之和,因为slot可以重用,当一个变量超出作用域了,该slot又会给别的局部变量使用,编译器会根据作用域计算max_locals。
  • code_length: 编译器编译后的字节码长度
  • code: 用于存储字节码指令的一系列字节流,每个指令是一个u1类型的单字节,当虚拟机读到该字节时,就可以知道是什么指令,知道是什么指令,就知道指令需要什么操作数,继续读就可以了,这里类似于汇编,u1的取值范围是0~255,可以表达256条指令。Java虚拟机规范中定义了约200条指令,参看Instructions。关于这部分内容以后再写博客介绍了。
  • exception_table_length:异常表的长度
  • exception_table: 异常表对于Code来说并不是必须存在的,所以上述长度也可以为0,异常表有4个属性,代表着如果在start_pc到end_pc之间出现catch_type类型的异常,就跳转到handler_pc所指向的行处理。

Exceptions属性

Exceptions属性在方法表中与Code属性平级,注意和上面Code中的异常表不同,Exceptions属性的作用是列出方法可能抛出的异常,Exceptions属性表的结构:

1
2
3
4
5
6
复制代码Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
  • number_of_exceptions: 可能抛出的异常种类的个数
  • exception_index_table:指向常量池中CONSTANT_Class_info的索引

LineNumberTable属性

LineNumber用来记录Java源码与字节码行号之间的对应关系,我们在编译代码时也可以使用-g: none或-g: line来取消生成这个属性,不过在调试代码时就看不到行号了,也无法打断点。

LineNumberTable的数据结构如下:

1
2
3
4
5
6
7
8
复制代码LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}

我们主要看line_number_table,start_pc是字节码行号,line_number是Java源码行号。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,我们在编译代码时也可以使用-g: none或-g: vars来取消生成这个属性,但是如果取消的话,IDE会用arg0,arg1这样的参数来取代原有的参数名,导致调试时不清晰。
LocalVariableTable的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}

主要介绍local_variable_table:

  • start_pc和length: 分别代表了这个局部变量的生命周期开始的字节码偏移量以及作用范围覆盖的长度
  • name_index和descriptor_index:分别指向代表局部变量名称和局部变量描述符的常量
  • index: 是该局部变量在局部变量表中的slot位置,如果变量时double 或者long类型的,占用的slot为index和index+1两个。

ConstantValue属性

ConstantValue是一个定长属性,用来通知虚拟机为静态变量赋值,如果同时定义了int x=3;和static int y=3;则虚拟机为x,y赋值的时机不同,对于x,是在实例构造器<init>中进行的,而static类型的变量,则会在类构造器<clinit>方法中或者使用ConstantValue属性。
目前javac编译器的规则是,如果同时有final和static修饰,则是使用ConstantValue属性,只有static时,并且变量类型是基本类型或者String时,就会在<clinit>中进行初始化。

InnerClasses属性

如果类中定义了内部类,则会使用InnerClasses属性来记录内部类和宿主的关系。
InnerClasses的数据结构如下:

1
2
3
4
5
6
7
8
9
10
复制代码InnerClasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes; //记录有多少个内部类
{ u2 inner_class_info_index;
u2 outer_class_info_index;
u2 inner_name_index;
u2 inner_class_access_flags;
} classes[number_of_classes];
}

还是只看classes字段,inner_class_info_index指向内部类的符号引用,outer_class_info_index指向宿主类的符号引用,inner_name_index指向内部类的名称,如果是匿名内部类,则为0,inner_class_access_flags是内部类的访问标志,见下表:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 Marked or implicitly public in source.
ACC_PRIVATE 0x0002 Marked private in source.
ACC_PROTECTED 0x0004 Marked protected in source.
ACC_STATIC 0x0008 Marked or implicitly static in source.
ACC_FINAL 0x0010 Marked final in source.
ACC_INTERFACE 0x0200 Was an interface in source.
ACC_ABSTRACT 0x0400 Marked or implicitly abstract in source.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

还有其它的一些属性,如果想了解,可以看一下参考资料。

参考资料:

  1. Java Virtual Machine Specification
  2. Java虚拟机:JVM高级特性与最佳实践

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图解红黑树

发表于 2017-11-20

红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。在了解红黑树之前我们需要简述一下二叉查找树。

BST

二叉查找树,也称有序二叉树,是指一棵空树或者具有以下性质的二叉树:

  • 左子节点的值比父节点小

  • 右子节点的值比父节点大

  • 任意节点的左右字树也分别为二叉查找树

  • 没有键值相等的点

    BST
    BST
    在理想情况下,二叉查找树增删改查的时间复杂度为O(logN),但是若是二叉树极度不平衡,比如下图这样形成了一个线性链后,就会产生最坏运行情况O(N).

最坏运行复杂度
最坏运行复杂度
基于BST存在的问题,平衡二叉树产生了,典型的有AVL树和红黑树,因为AVL是严格的平衡二叉树,但是插入和删除的性能较差,所以在实际生产环境中不如红黑树应用广泛。

RBTree

红黑树的应用非常广泛,常见的函数库,如C++中的map,multimap,以及Java中的TreeMap,TreeSet, Java8中的HashMap的实现也采用了红黑树。

红黑树从本质上来说就是一颗二叉查找树,但是在二叉树的基础上增加了着色相关的性质,使得红黑树可以保证相对平衡,从而保证红黑树的增删改查的时间复杂度最坏也能达到O(log N)。

下面是红黑树最重要的5条性质,后面需要正常回来查看:

  1. 每个节点要么是黑的,要么是红的
  2. 根节点是黑的
  3. 叶节点是黑的
  4. 如果一个节点是红的,他的两个儿子节点都是黑的
  5. 对于任一节点而言,其到叶节点树尾端NIL指针的每一条路径都包含相同数目的黑节点

红黑树(图片引自维基百科)
红黑树(图片引自维基百科)
上图是一棵典型的红黑树,红黑树的5条特性确保了从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,使得整棵树大致上是平衡的。树上的增删改查操作的最坏情况时间都与树的高度成正比,所以红黑树在最坏情况下也是高效的。

在红黑树中一般用黑的NIL节点表示叶节点,不包含值,只是标志该分支结束,有时候绘图中会直接省略。

在正式介绍红黑树插入和删除操作前,需要先了解红黑树的旋转操作。

RBTree的旋转操作

当在含n个关键字的红黑树上进行insert和delete操作时,修改后的树可能不满足上面给出的5个红黑树的基本特性,所以需要改变树中的某些节点的颜色以及指针结构。 这些指针结构的修改是通过旋转完成的,旋转分为左旋和右旋:

旋转操作
旋转操作
当在某个节点x上做左旋时,我们假设它的右孩子是y节点并且不为NIL。左旋以x到y之间的轴为支撑,左旋后,y成为该局部新的根,x成为y的做孩子,而y的左孩子成为x的右孩子,即图中的β。我们以一段伪代码说明左旋的过程:

1
2
3
4
5
6
7
8
9
10
11
stylus复制代码y<-right[x]   //把x的右儿子保存为y
right[x]<-left[y] //把y的左儿子给x作为右儿子
p[left[y]]<-x //把x设为y的左儿子的爸爸,这一步与上一步对应,因为指针都是双向的
p[y]<-p[x] //把x的爸爸设定为y的爸爸
if p[x]=nil[T] //如果x的爸爸本来就是空的
then root[T]<-y //那么y就变成了根节点
else if x=left[p[x]] //否则,如果原来x是它爸爸的左儿子
then left[p[x]]<-y //就把y设为原来x爸爸的左儿子
else right[p[x]]<-y //不然就把y设为原来x爸爸的右儿子
left[y]<-x //x这时候转下来成为y的左儿子
p[x]<-y //y 也就成了x的爸爸了

在实际的树中,旋转如下所示:

旋转前
旋转前

旋转后,18代替了11成为了7的右儿子,11,成了18的左儿子,18原先的儿子,即以14为根节点的左子树成了11的右儿子,如下所示:

旋转后
旋转后
右旋操作与左旋类似,读者们可以自行试着写出伪代码。

RBTree的插入操作

红黑树的插入与BST的插入方式是一样的,也是通过不断比较大小,插入到合适位置,只不过插入后需要做调整,以满足红黑树的5条特性。

先看一下BST的插入代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pgsql复制代码 public BinaryNode<T> insert(T t,BinaryNode<T> node)
{
if(node==null)
{
return new BinaryNode<T>(t, null, null);
}
int result = t.compareTo(node.data);
if(result<0)
node.left= insert(t,node.left);
else if(result>0)
node.right= insert(t,node.right);
else
;//doNothing
return node;
}

新插入的节点总是设为红色的,所以如果父节点为黑色,就不需要修复,因为没有任何性质被改变,所以只有在父节点为红色节点时需要做修复操作。

修复操作一共分为3种情况:

情况1 :当前节点的父节点是红色,且祖父节点的另一个子节点(叔叔节点)也是红色

此时,我们只考虑当前节点是父节点的左儿子,因为当前节点和父节点都是红色,不满足红黑树的特性4 (如果一个节点是红的,他的两个儿子节点都是黑的)。

对策 :把父节点和叔叔节点变黑,爷爷节点涂红,然后把当前节点指针给到爷爷,让爷爷节点那层继续循环,接受红黑树特性检测。

1.PNG
1.PNG
新插入节点1,父亲2和叔叔6都是红的,所以把2和6都变成黑的, 爷爷变成红的。

情况2: 当前节点的父节点是红的,叔叔节点是黑的,当前节点是父节点的右子树。

对策:当前节点的父节点作为新的当前节点,以新当前指点为支点左旋

接着上面的情况1, 在情况1的操作之后,当前节点为4, 但是4的父节点3还是红的,仍然不满足红黑树的特性4, 由于叔叔节点7是黑的,且4为3的右儿子,所以满足情况2,这时候把当前节点转移到3,以3为支点左旋。4变为爷爷5的新左儿子,而原来的3下去了,成为4的左儿子。4原有的左儿子(2-1分支)给3做了右儿子。

2.PNG
2.PNG

情况3:当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左儿子

对策: 父节点变黑,祖父变红,以祖父节点为支点右旋

接着上面的情况2, 当前节点变成了3,由于父节点4为红,叔叔节点7 为黑,且4为5的左儿子,所以满足情况3,所以需要将父节点4变黑,祖父5变红,并且右旋,右旋后,4 的右儿子6跟了5做了左儿子,5成了4的右儿子。如图所示:

3.PNG
3.PNG
这样红黑树重新恢复了平衡。上面的例子里正好遇到了三种基本情况,实际操作中也许一步就成功了,简化的来看,也就是下面三种基本情况:
case1 :

父红叔红
父红叔红
case2 :
父红, 左子
父红, 左子
case3 :
父红, 右子
父红, 右子
当前节点为父节点的右节点的时候,先旋转成为左子,即case2的样子,再右旋。

修复操作整体来说是一个向上回溯的过程,就拿我们举得例子来说,情况1操作后,当前节点向上移到了4, 此时又会检测4 和它的父节点之间性质是否符合红黑树的5条特性,不对的话继续修复红黑树,直到当前节点转移到root节点,且root节点为黑色,修复操作结束。

RBTree的删除操作

删除操作首先也需要做普通BST的删除操作,删除操作会删除对应的节点,叶子节点就会直接删除,如果是非叶子节点,就会用中序遍历的后继节点来顶替要删除的节点,有的书上也会用前驱节点来顶替。删除后也需要做修复操作,来满足红黑树的特性。

首先也是看一下BST删除节点的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码public BinaryNode<T> remove(T t,BinaryNode<T> node)
{
if(node == null)
return node;//没有找到,doNothing
int result = t.compareTo(node.data);
if(result>0)
node.right = remove(t,node.right);
else if(result<0)
node.left = remove(t,node.left);
else if(node.left!=null&&node.right!=null)
{ //待删除节点有两个儿子节点时,用右儿子中的最小元素填充待删除节点
node.data = findMin(node.right).data;
node.right = remove(node.data,node.right);
}
else //只有一个儿子的时候,把父节点的儿子指针指向儿子的该独生子
node = (node.left!=null)?node.left:node.right;
return node;
}

删除修复操作在遇到被删除的节点是红色节点或者到达root节点时,修复操作完毕。

在删除一个节点后,如果删除的节点时红色节点,那么红黑树的性质并不会被影响,此时不需要修正,如果删除的是黑色节点,原红黑树的性质就会被改变,此时我们需要做修正。

当黑色节点被删除后,假设该节点为y,会产生3个问题:

  1. 如果y原来是根节点,而y的一个红色孩子成为新的根,则违反了性质2
  2. 如果y的子节点和y的父节点都是红色,那么y被删除后,两个连续的红色节点连接起来,违反了性质4
  3. 删除y将导致先前包含y的任何路径上的黑节点个数少1,性质5被破坏

现在我们假设,顶替删除节点的那个后来节点,继承了被删除的黑色节点的那层黑色,也就是说顶替的节点具有双重颜色,如果原来是黑色,那么现在就是黑+黑,如果原来是红色,现在就是红+黑。因为有了这层额外的黑色,所以性质5还是能保持的,现在只需要恢复它的性质即可。

  • 如果当前节点时红+黑色
    直接把当前节点染成黑色,此时红黑树性质全部恢复
  • 如果当前节点时黑+黑且是根节点,此时什么都不用做,直接结束
  • 如果当前节点时黑+黑,但是不是根节点,那么又可以分为以下4种情况

设删除节点为x节点

情况1:x节点时黑+黑,且x的兄弟节点是红色(x的父节点和兄弟节点的子节点都是黑色)

x节点的兄弟节点为红色(x节点的父节点和兄弟节点的子节点都是黑色)
x节点的兄弟节点为红色(x节点的父节点和兄弟节点的子节点都是黑色)

因为兄弟节点7必须有黑色孩子,我们可以改变4和7的颜色,再对4做一次左旋,而且红黑性质继续保存。完成这两个操作后,尽管所有路径上黑色节点的数目没有改变,但现在删除节点有了一个黑色的兄弟和一个红色的父亲(它的新兄弟是黑色因为它是原先红7的一个儿子),所以我们可以接下去按情形2、情形3或情形4来处理。

情况2:x节点时黑+黑,x的兄弟节点时黑色(x的兄弟节点的两个孩子都是黑色)

x的兄弟节点时黑色(x的兄弟节点的两个孩子都是黑色)
x的兄弟节点时黑色(x的兄弟节点的两个孩子都是黑色)
此时我们把x的兄弟节点转变为红色,设置x的父节点为新的当前节点。
其实这样做的思想是把原先x中的一个黑色属性上移。原先的x变成单纯的黑节点,而它的父节点4此时变成了红+黑,如果4原来就是黑,那么此时变成黑+黑。此时左边分支的黑节点数没有变化,但是右边4,7那一条分支,黑节点数增加了1,因为此时4也包含黑属性,所以需要通过减1个黑色节点,因为兄弟节点7的子节点都是黑的,所以直接把7 变成红的。
经过上面的步骤,黑色属性转移到4中去了,这时候继续对4进行处理。

情况3:x节点是黑+黑节点,x的兄弟节点时黑色(x的兄弟节点的左儿子是红,右儿子是黑)

x的兄弟节点时黑色(x的兄弟节点的左儿子是红,右儿子是黑)
x的兄弟节点时黑色(x的兄弟节点的左儿子是红,右儿子是黑)

此时我们把x的兄弟节点的左儿子设为黑色,将兄弟节点设为红色,再对兄弟节点进行右旋。并且重新设置旋转后x的兄弟节点,此时是5. 其实这一步只是一个中间状态,并且不是平衡的,目的是为了得到情况4的状态

情况4:x节点是黑+黑节点,x的兄弟节点时黑色(x的兄弟节点的右儿子是红,左儿子随意)

x的兄弟节点时黑色(x的兄弟节点的右儿子是红,左儿子随意)
x的兄弟节点时黑色(x的兄弟节点的右儿子是红,左儿子随意)
此时,把x的父节点的颜色赋给x的兄弟节点,把父节点设为黑色,将x的兄弟节点的右子节点设为黑色,再对x的父节点进行左旋。这一步操作的真正的节点借调操作,通过将兄弟节点以及兄弟节点的右节点借调过来,并将兄弟节点的右子节点变成红色来达到借调两个黑节点的目的,这样的话,整棵树还是符合红黑树的定义的

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…938939940…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%