python算法與數據結構-順序表(37)

 

1、順序表介紹

  順序表是最簡單的一種線性結構,邏輯上相鄰的數據在計算機內的存儲位置也是相鄰的,可以快速定位第幾個元素,中間不允許有空,所以插入、刪除時需要移動大量元素。順序表可以分配一段連續的存儲空間Maxsize,用elem記錄基地址,用length記錄實際的元素個數,即順序表的長度, 

  上圖1表示的是順序表的基本形式,數據元素本身連續存儲,每個元素所佔的存儲單元大小固定相同,元素的下標是其邏輯地址,而元素存儲的物理地址(實際內存地址)可以通過存儲區的起始地址Loc (e0)加上邏輯地址(第i個元素)與存儲單元大小(c)的乘積計算而得,即:Loc(element i) = Loc(e0) + c*i

所以、訪問指定元素時無需從頭遍歷,通過計算便可獲得對應地址,其時間複雜度為O(1)。

  如果元素的大小不統一,則須採用圖2的元素外置的形式,將實際數據元素另行存儲,而順序表中各單元位置保存對應元素的地址信息(即鏈接)。由於每個鏈接所需的存儲量相同,通過上述公式,可以計算出元素鏈接的存儲位置,而後順着鏈接找到實際存儲的數據元素。注意,圖2中的c不再是數據元素的大小,而是存儲一個鏈接地址所需的存儲量,這個量通常很小。

圖2這樣的順序表也被稱為對實際數據的索引,這是最簡單的索引結構。

2、順序表的結構 

  

  一個順序表的完整信息包括兩部分,一部分是表中的元素集合,另一部分是為實現正確操作而需記錄的信息,即有關表的整體情況的信息,這部分信息主要包括元素存儲區的容量和當前表中已有的元素個數兩項。

3、順序表的兩種基本實現方式

  1為一體式結構,存儲表信息的單元與元素存儲區以連續的方式安排在一塊存儲區里,兩部分數據的整體形成一個完整的順序表對象。一體式結構整體性強,易於管理。但是由於數據元素存儲區域是表對象的一部分,順序表創建后,元素存儲區就固定了。

  2為分離式結構,表對象里只保存與整個表有關的信息(即容量和元素個數),實際數據元素存放在另一個獨立的元素存儲區里,通過鏈接與基本表對象關聯。

 4、元素存儲區替換

  一體式結構由於順序表信息區與數據區連續存儲在一起,所以若想更換數據區,則只能整體搬遷,即整個順序表對象(指存儲順序表的結構信息的區域)改變了。分離式結構若想更換數據區,只需將表信息區中的數據區鏈接地址更新即可,而該順序表對象不變。

5、元素存儲區擴充

  採用分離式結構的順序表,若將數據區更換為存儲空間更大的區域,則可以在不改變表對象的前提下對其數據存儲區進行了擴充,所有使用這個表的地方都不必修改。只要程序的運行環境(計算機系統)還有空閑存儲,這種表結構就不會因為滿了而導致操作無法進行。人們把採用這種技術實現的順序表稱為動態順序表,因為其容量可以在使用中動態變化。

擴充的兩種策略

  • 每次擴充增加固定數目的存儲位置,如每次擴充增加10個元素位置,這種策略可稱為線性增長。

    特點:節省空間,但是擴充操作頻繁,操作次數多。

  • 每次擴充容量加倍,如每次擴充增加一倍存儲空間。

    特點:減少了擴充操作的執行次數,但可能會浪費空間資源。以空間換時間,推薦的方式。

6、順序表的增刪改查操作的Python代碼實現

# 創建順序表
class Sequence_Table():
    
    # 初始化
    def __init__(self):
        self.date = [None]*100
        self.length = 0
    
    # 判斷是否已經滿了
    def isFull(self):
        if self.length>100:
            print("該順序表已滿,無法添加元素")
            return 1
        else:
            return 0
    
    # 按下錶索引查找
    def selectByIndex(self,index):
        if index>=0 and index<=self.length-1:
            return self.date[index]
        else:
            print("你輸入的下標不對,請重新輸入\n")
            return 0
        
    # 按元素查下標
    def selectByNum(self,num):
        isContain = 0
        for i in range(0,self.length):
            if self.date[i] == num:
                isContain = 1
                print("你要查找的元素下標是%d\n"%i)
        if isContain == 0:
            print("沒有找你你要的數據")
    
    # 追加數據
    def addNum(self,num):
        if self.isFull() == 0:
            self.date[self.length] = num
            self.length += 1
            
    # 打印順序表
    def printAllNum(self):
        for i in range(self.length):
            print("a[%s]=%s"%(i,self.date[i]),end=" ")
        print("\n")
        
    # 按下標插入數據
    def insertNumByIndex(self,num,index):
        if index<0 or index>self.length:
            return 0
        self.length += 1
        for i in range(self.length-1,index,-1):
            temp = self.date[i]
            self.date[i] = self.date[i-1]
            self.date[i-1] = temp
        self.date[index] = num
        return 1
    # 按下標刪除數據
    def delectNumByIndex(self,index):
        if self.length <= 0:
            print("該順序表內沒有數據,不用刪除")
            
        for i in range(index,self.length-1):
            temp = self.date[i]
            self.date[i] = self.date[i + 1]
            self.date[i + 1] = temp
        self.date[self.length-1] = 0
        self.length -= 1

def main():
    # 創建順序表對象
    seq_t = Sequence_Table()
    
    # 插入三個元素
    seq_t.addNum(1)
    seq_t.addNum(2)
    seq_t.addNum(3)
    
    # 打印驗證
    seq_t.printAllNum()
    
    # 按照索引查找
    num = seq_t.selectByIndex(2)
    print("你要查找的數據是%d\n" % num)
    
    # 按照索引插入數據
    seq_t.insertNumByIndex(4, 1)
    seq_t.printAllNum()
    
    # 按照数字查下標
    seq_t.selectByNum(4)
    
    #刪除數據
    seq_t.delectNumByIndex(1)
    seq_t.printAllNum()
     
if __name__ == "__main__":
    main()

運行結果為:

a[0]=1 a[1]=2 a[2]=3 

你要查找的數據是3

a[0]=1 a[1]=4 a[2]=2 a[3]=3 

你要查找的元素下標是1

a[0]=1 a[1]=2 a[2]=3 

7、順序表的增刪改查操作的C語言代碼實現

#include<stdio.h>
// 1、定義順序表的儲存結構
typedef struct
{
    //用數組存儲線性表中的元素
    int data[100];
    // 順序表中的元素個數
    int length;
}Sequence_table,*p_Sequence_table;

// 2、順序表的初始化,
void initSequenceTable(p_Sequence_table T)
{
    // 判斷傳過來的表是否為空,為空直接退出
    if (T == NULL)
    {
        return;
    }
    // 設置默認長度為0
    T->length = 0;
}

// 3、求順序表的長度
int lengthOfSequenceTable(p_Sequence_table T)
{
    if (T==NULL)
    {
        return 0;
    }
    return T->length;
}

// 4、判斷順序表是否已滿
int isFull(p_Sequence_table T)
{
    if (T->length>=100)
    {
        printf("該順序表已經裝滿,無法再添加元素");
        return 1;
    }
    return 0;
}

// 5、按序號查找
int selectSequenceTableByIndex(p_Sequence_table T,int index)
{
    if (index>=0&&index<=T->length-1)
    {
        return T->data[index];
    }
    printf("你輸入的序號不對,請重新輸入\n");
    return 0;
}

// 6、按內容查找是否存在
void selectSequenceTableByNum(p_Sequence_table T,int num)
{
    int isContain = 0;
    for (int i=0; i<T->length; i++)
    {
        if (T->data[i] == num)
        {
            isContain = 1;
            printf("你要找的元素的下標是:%d\n",i);
        }
    }
    if (isContain == 0)
    {
        printf("沒有找到你要的數據\n");
    }
}

// 7、添加元素(在隊尾添加)
void addNumber(p_Sequence_table T,int num)
{
    // 順序表還沒有滿的時候
    if (isFull(T) == 0)
    {
        T->data[T->length] = num;
        T->length++;
    }
}

// 8、順序表的遍歷
void printAllNumOfSequenceTable(p_Sequence_table T)
{
    for (int i = 0; i<T->length; i++)
    {
        printf("T[%d]=%d ",i,T->data[i]);
    }
    printf("\n");
}

//9、插入操作
int insertNumByIndex(p_Sequence_table T, int num,int index)
{
    if (index<0||index>T->length)
    {
        return 0;
    }
    T->length++;
    for (int i = T->length-1; i>index; i--)
    {
        int temp = T->data[i];
        T->data[i] = T->data[i-1];
        T->data[i-1] = temp;
    }
    T->data[index] = num;
    return 1;
}

// 10、刪除元素
void delectNum(p_Sequence_table T,int index)
{
    if (T->length <= 0)
    {
        printf("該順序表中沒有數據,不用刪除");
    }
    for (int i = index;i<T->length-1; i++)
    {
        int temp = T->data[i];
        T->data[i] = T->data[i+1];
        T->data[i+1] = temp;
    }
    T->data[T->length-1] = 0;
    T->length--;
}



int main(int argc, const char * argv[]) {
    
    // 創建順序表的結構體
    Sequence_table seq_t;
    // 初始化
    initSequenceTable(&seq_t);
    // 添加數據
    addNumber(&seq_t, 1);
    addNumber(&seq_t, 2);
    addNumber(&seq_t, 3);
    // 打印驗證
    printAllNumOfSequenceTable(&seq_t);
    // 根據索引下標查內容
    int num = selectSequenceTableByIndex(&seq_t, 2);
    printf("你查的數據是:%d\n",num);
    // 插入
    insertNumByIndex(&seq_t, 4, 1);
    printAllNumOfSequenceTable(&seq_t);
    // 根據內容查下標
    selectSequenceTableByNum(&seq_t, 4);
    // 根據下標刪除數據
    delectNum(&seq_t, 1);
    printAllNumOfSequenceTable(&seq_t);
    return 0;
}

運行結果為:

T[0]=1 T[1]=2 T[2]=3 
你查的數據是:3
T[0]=1 T[1]=4 T[2]=2 T[3]=3 
你要找的元素的下標是:1
T[0]=1 T[1]=2 T[2]=3 

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

SpringBoot之ApplicationContextInitializer的理解和使用

一、 ApplicationContextInitializer 介紹

  首先看spring官網的介紹:

   翻譯一下:

  • 用於在spring容器刷新之前初始化Spring ConfigurableApplicationContext的回調接口。(剪短說就是在容器刷新之前調用該類的 initialize 方法。並將 ConfigurableApplicationContext 類的實例傳遞給該方法)
  • 通常用於需要對應用程序上下文進行編程初始化的web應用程序中。例如,根據上下文環境註冊屬性源或激活配置文件等。
  • 可排序的(實現Ordered接口,或者添加@Order註解)

  看完這段解釋,為了講解方便,我們先看自定義 ApplicationContextInitializer 的三種方式。再通過SpringBoot的源碼,分析生效的時間以及實現的功能等。

二、三種實現方式

  首先新建一個類 MyApplicationContextInitializer 並實現 ApplicationContextInitializer 接口。

1 public class MyApplicationContextInitializer implements ApplicationContextInitializer {
2     @Override
3     public void initialize(ConfigurableApplicationContext applicationContext) {
4         System.out.println("-----MyApplicationContextInitializer initialize-----");
5     }
6 }

  2.1、mian函數中添加

  優雅的寫一個SpringBoot的main方法

1 @SpringBootApplication
2 public class MySpringBootApplication {
3     public static void main(String[] args) {
4         SpringApplication application = new SpringApplication(MySpringBootApplication.class);
5         application.addInitializers(new MyApplicationContextInitializer());
6         application.run(args);
7     }
8 }

 

  運行,查看控制台:生效了

  

  2.2、配置文件中配置

context.initializer.classes=org.springframework.boot.demo.common.MyApplicationContextInitializer 

 

  

  2.3、SpringBoot的SPI擴展—META-INF/spring.factories中配置

org.springframework.context.ApplicationContextInitializer=org.springframework.boot.demo.common.MyApplicationContextInitializer

 

  

 

三、排序問題

  如圖所示改造一下mian方法。打一個斷點,debug查看排序情況。

  

  給 MyApplicationContextInitializer 加上Order註解:我們指定其擁有最高的排序級別。(越高越早執行)

1 @Order(Ordered.HIGHEST_PRECEDENCE)
2 public class MyApplicationContextInitializer implements ApplicationContextInitializer{
3     @Override
4     public void initialize(ConfigurableApplicationContext applicationContext) {
5         System.out.println("-----MyApplicationContextInitializer initialize-----");
6     }
7 }

 

  下面我們通過debug分別驗證二章節中提到的三種方法排序是否都是可以的。

  首先驗證2.1章節中採用的main函數中添加:debug,斷點處查看 application.getInitializers() 這行代碼的結果可見,排序生效了。

  

  然後再分別驗證2.2和2.3章節中的方法。排序都是可以實現的。

  然而當採用2.3中的SPI擴展的方式,排序指定 @Order(Ordered.LOWEST_PRECEDENCE) 排序並沒有生效。當然採用實現Ordered接口的方式,排序驗證結果都是一樣的。

 四、通過源碼分析ApplicationContextInitializer何時被調用

  debug差看上文中自定的 MyApplicationContextInitializer 的調用棧。

  

  可見 ApplicationContextInitializer 在容器刷新前的準備階段被調用。 refreshContext(context); 

  在SpringBoot的啟動函數中, ApplicationContextInitializer 

 1     public ConfigurableApplicationContext run(String... args) {
 2         //記錄程序運行時間
 3         StopWatch stopWatch = new StopWatch();
 4         stopWatch.start();
 5         // ConfigurableApplicationContext Spring 的上下文
 6         ConfigurableApplicationContext context = null;
 7         Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
 8         configureHeadlessProperty();
 9         //從META-INF/spring.factories中獲取監聽器
10         //1、獲取並啟動監聽器
11         SpringApplicationRunListeners listeners = getRunListeners(args);
12         listeners.starting();
13         try {
14             ApplicationArguments applicationArguments = new DefaultApplicationArguments(
15                     args);
16             //2、構造容器環境
17             ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
18             //處理需要忽略的Bean
19             configureIgnoreBeanInfo(environment);
20             //打印banner
21             Banner printedBanner = printBanner(environment);
22             ///3、初始化容器
23             context = createApplicationContext();
24             //實例化SpringBootExceptionReporter.class,用來支持報告關於啟動的錯誤
25             exceptionReporters = getSpringFactoriesInstances(
26                     SpringBootExceptionReporter.class,
27                     new Class[]{ConfigurableApplicationContext.class}, context);
28             //4、刷新容器前的準備階段
29             prepareContext(context, environment, listeners, applicationArguments, printedBanner);
30             //5、刷新容器
31             refreshContext(context);
32             //刷新容器后的擴展接口
33             afterRefresh(context, applicationArguments);
34             stopWatch.stop();
35             if (this.logStartupInfo) {
36                 new StartupInfoLogger(this.mainApplicationClass)
37                         .logStarted(getApplicationLog(), stopWatch);
38             }
39             listeners.started(context);
40             callRunners(context, applicationArguments);
41         } catch (Throwable ex) {
42             handleRunFailure(context, ex, exceptionReporters, listeners);
43             throw new IllegalStateException(ex);
44         }
45 
46         try {
47             listeners.running(context);
48         } catch (Throwable ex) {
49             handleRunFailure(context, ex, exceptionReporters, null);
50             throw new IllegalStateException(ex);
51         }
52         return context;
53     }

 

   然後看在 refreshContext(context); 具體是怎麼被調用的。

1 private void prepareContext(ConfigurableApplicationContext context,
2                             ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
3                             ApplicationArguments applicationArguments, Banner printedBanner) {
4     context.setEnvironment(environment);
5     postProcessApplicationContext(context);
6     applyInitializers(context);
7     ...
8 }

 

   然後在 applyInitializers 中遍歷調用每一個被加載的 ApplicationContextInitializer 的  initialize(context);  方法,並將 ConfigurableApplicationContext 的實例傳遞給 initialize 方法。

1 protected void applyInitializers(ConfigurableApplicationContext context) {
2     for (ApplicationContextInitializer initializer : getInitializers()) {
3         Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
4                 initializer.getClass(), ApplicationContextInitializer.class);
5         Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
6         initializer.initialize(context);
7     }
8 }

 

  OK,到這裏通過源碼說明了 ApplicationContextInitializer 是何時及如何被調用的。

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

從0到1:全面理解RPC遠程調用

上一篇關於 WSGI 的硬核長文,不知道有多少同學,能夠從頭看到尾的,不管你們有沒有看得很過癮,反正我是寫得很爽,總有一種將一樣知識吃透了的錯覺。

今天我又給自己挖坑了,打算將 rpc 遠程調用的知識,好好地梳理一下,花了周末整整两天的時間。

什麼是RPC呢?

百度百科給出的解釋是這樣的:“RPC(Remote Procedure Call Protocol)——遠程過程調用協議,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議”。這個概念聽起來還是比較抽象,沒關係,繼續往後看,後面概念性的東西,我會講得足夠清楚,讓你完全掌握 RPC 的基礎內容。在後面的篇章中還會結合其在 OpenStack 中實際應用,一步一步揭開 rpc 的神秘面紗。

有的讀者,可能會問,為啥我舉的例子老是 OpenStack 里的東西呢?

因為每個人的業務中接觸的框架都不一樣(我主要接觸的就是 OpenStack 框架),我無法為每個人去定製寫一篇文章,但其技術原理都是一樣的。即使如此,我也會儘力將文章寫得通用,不會因為你沒接觸過 OpenStack 而成為你理解 rpc 的瓶頸。

01. 既 REST,何 RPC ?

在 OpenStack 里的進程間通信方式主要有兩種,一種是基於HTTP協議的RESTFul API方式,另一種則是RPC調用。

那麼這兩種方式在應用場景上有何區別呢?

有使用經驗的人,就會知道:

  • 前者(RESTful)主要用於各組件之間的通信(如nova與glance的通信),或者說用於組件對外提供調用接口
  • 而後者(RPC)則用於同一組件中各個不同模塊之間的通信(如nova組件中nova-compute與nova-scheduler的通信)。

關於OpenStack中基於RESTful API的通信方式主要是應用了WSGI,這個知識點,我在前一篇文章中,有深入地講解過,你可以點擊查看。

對於不熟悉 OpenStack 的人,也別擔心聽不懂,這樣吧,我給你提兩個問題:

  1. RPC 和 REST 區別是什麼?
  2. 為什麼要採用RPC呢?

第一個問題:RPC 和 REST 區別是什麼?

你一定會覺得這個問題很奇怪,是的,包括我,但是你在網絡上一搜,會發現類似對比的文章比比皆是,我在想可能很多初學者由於基礎不牢固,才會將不相干的二者拿出來對比吧。既然是這樣,那為了讓你更加了解陌生的RPC,就從你熟悉得不能再熟悉的 REST 入手吧。

01、所屬類別不同

REST,是Representational State Transfer 的簡寫,中文描述表述性狀態傳遞(是指某個瞬間狀態的資源數據的快照,包括資源數據的內容、表述格式(XML、JSON)等信息。)

REST 是一種軟件架構風格。 這種風格的典型應用,就是HTTP。其因為簡單、擴展性強的特點而廣受開發者的青睞。

而RPC 呢,是 Remote Procedure Call Protocol 的簡寫,中文描述是遠程過程調用,它可以實現客戶端像調用本地服務(方法)一樣調用服務器的服務(方法)。

RPC 是一種基於 TCP 的通信協議,按理說它和REST不是一個層面上的東西,不應該放在一起討論,但是誰讓REST這麼流行呢,它是目前最流行的一套互聯網應用程序的API設計標準,某種意義下,我們說 REST 可以其實就是指代 HTTP 協議。

02、使用方式不同

從使用上來看,HTTP 接口只關注服務提供方,對於客戶端怎麼調用並不關心。接口只要保證有客戶端調用時,返回對應的數據就行了。而RPC則要求客戶端接口保持和服務端的一致。

  • REST 是服務端把方法寫好,客戶端並不知道具體方法。客戶端只想獲取資源,所以發起HTTP請求,而服務端接收到請求后根據URI經過一系列的路由才定位到方法上面去
  • PRC是服務端提供好方法給客戶端調用,客戶端需要知道服務端的具體類,具體方法,然後像調用本地方法一樣直接調用它。

03、面向對象不同

從設計上來看,RPC,所謂的遠程過程調用 ,是面向方法的 ,REST:所謂的 Representational state transfer ,是面向資源的,除此之外,還有一種叫做 SOA,所謂的面向服務的架構,它是面向消息的,這個接觸不多,就不多說了。

04、序列化協議不同

接口調用通常包含兩個部分,序列化和通信協議。

通信協議,上面已經提及了,REST 是 基於 HTTP 協議,而 RPC 可以基於 TCP/UDP,也可以基於 HTTP 協議進行傳輸的。

常見的序列化協議,有:json、xml、hession、protobuf、thrift、text、bytes等,REST 通常使用的是 JSON或者XML,而 RPC 使用的是 JSON-RPC,或者 XML-RPC。

通過以上幾點,我們知道了 REST 和 RPC 之間有很明顯的差異。

第二個問題:為什麼要採用RPC呢?

那到底為何要使用 RPC,單純的依靠RESTful API不可以嗎?為什麼要搞這麼多複雜的協議,渣渣表示真的學不過來了。

關於這一點,以下幾點僅是我的個人猜想,僅供交流哈:

  1. RPC 和 REST 兩者的定位不同,REST 面向資源,更注重接口的規範,因為要保證通用性更強,所以對外最好通過 REST。而 RPC 面向方法,主要用於函數方法的調用,可以適合更複雜通信需求的場景。
  2. RESTful API客戶端與服務端之間採用的是同步機制,當發送HTTP請求時,客戶端需要等待服務端的響應。當然對於這一點是可以通過一些技術來實現異步的機制的。
  3. 採用RESTful API,客戶端與服務端之間雖然可以獨立開發,但還是存在耦合。比如,客戶端在發送請求的時,必須知道服務器的地址,且必須保證服務器正常工作。而 rpc + ralbbimq中間件可以實現低耦合的分佈式集群架構。

說了這麼多,我們該如何選擇這兩者呢?我總結了如下兩點,供你參考:

  • REST 接口更加規範,通用適配性要求高,建議對外的接口都統一成 REST(也有例外,比如我接觸過 zabbix,其 API 就是基於 JSON-RPC 2.0協議的)。而組件內部的各個模塊,可以選擇 RPC,一個是不用耗費太多精力去開發和維護多套的HTTP接口,一個RPC的調用性能更高(見下條)
  • 從性能角度看,由於HTTP本身提供了豐富的狀態功能與擴展功能,但也正由於HTTP提供的功能過多,導致在網絡傳輸時,需要攜帶的信息更多,從性能角度上講,較為低效。而RPC服務網絡傳輸上僅傳輸與業務內容相關的數據,傳輸數據更小,性能更高。

02. 實現遠程調用的三種方式

“遠程調用”意思就是:被調用方法的具體實現不在程序運行本地,而是在別的某個地方(分佈到各個服務器),調用者只想要函數運算的結果,卻不需要實現函數的具體細節。

01、基於 xml-rpc

Python實現 rpc,可以使用標準庫里的 SimpleXMLRPCServer,它是基於XML-RPC 協議的。

有了這個模塊,開啟一個 rpc server,就變得相當簡單了。執行以下代碼:

import SimpleXMLRPCServer

class calculate:
    def add(self, x, y):
        return x + y

    def multiply(self, x, y):
        return x * y

    def subtract(self, x, y):
        return abs(x-y)

    def divide(self, x, y):
        return x/y


obj = calculate()
server = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", 8088))
# 將實例註冊給rpc server
server.register_instance(obj)

print "Listening on port 8088"
server.serve_forever()

有了 rpc server,接下來就是 rpc client,由於我們上面使用的是 XML-RPC,所以 rpc clinet 需要使用xmlrpclib 這個庫。

import xmlrpclib

server = xmlrpclib.ServerProxy("http://localhost:8088")

然後,我們通過 server_proxy 對象就可以遠程調用之前的rpc server的函數了。

>> server.add(2, 3)
5
>>> server.multiply(2, 3)
6
>>> server.subtract(2, 3)
1
>>> server.divide(2, 3)
0

SimpleXMLRPCServer是一個單線程的服務器。這意味着,如果幾個客戶端同時發出多個請求,其它的請求就必須等待第一個請求完成以後才能繼續。

若非要使用 SimpleXMLRPCServer 實現多線程併發,其實也不難。只要將代碼改成如下即可。

from SimpleXMLRPCServer import SimpleXMLRPCServer
from SocketServer import ThreadingMixIn
class ThreadXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):pass

class MyObject:
    def hello(self):
        return "hello xmlprc"

obj = MyObject()
server = ThreadXMLRPCServer(("localhost", 8088), allow_none=True)
server.register_instance(obj)

print "Listening on port 8088"
server.serve_forever()

02、基於json-rpc

SimpleXMLRPCServer 是基於 xml-rpc 實現的遠程調用,上面我們也提到 除了 xml-rpc 之外,還有 json-rpc 協議。

那 python 如何實現基於 json-rpc 協議呢?

答案是很多,很多web框架其自身都自己實現了json-rpc,但我們要獨立這些框架之外,要尋求一種較為乾淨的解決方案,我查找到的選擇有兩種

第一種是 jsonrpclib

pip install jsonrpclib -i https://pypi.douban.com/simple

第二種是 python-jsonrpc

pip install python-jsonrpc -i https://pypi.douban.com/simple

先來看第一種 jsonrpclib

它與 Python 標準庫的 SimpleXMLRPCServer 很類似(因為它的類名就叫做 SimpleJSONRPCServer ,不明真相的人真以為它們是親兄弟)。或許可以說,jsonrpclib 就是仿照 SimpleXMLRPCServer 標準庫來進行編寫的。

它的導入與 SimpleXMLRPCServer 略有不同,因為SimpleJSONRPCServer分佈在jsonrpclib庫中。

服務端

from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer

server = SimpleJSONRPCServer(('localhost', 8080))
server.register_function(lambda x,y: x+y, 'add')
server.serve_forever()

客戶端

import jsonrpclib

server = jsonrpclib.Server("http://localhost:8080")

再來看第二種python-jsonrpc,寫起來貌似有些複雜。

服務端

import pyjsonrpc


class RequestHandler(pyjsonrpc.HttpRequestHandler):

    @pyjsonrpc.rpcmethod
    def add(self, a, b):
        """Test method"""
        return a + b

http_server = pyjsonrpc.ThreadingHttpServer(
    server_address=('localhost', 8080),
    RequestHandlerClass=RequestHandler
)
print "Starting HTTP server ..."
print "URL: http://localhost:8080"
http_server.serve_forever()

客戶端

import pyjsonrpc

http_client = pyjsonrpc.HttpClient(
    url="http://localhost:8080/jsonrpc"
)

還記得上面我提到過的 zabbix API,因為我有接觸過,所以也拎出來講講。zabbix API 也是基於 json-rpc 2.0協議實現的。

因為內容較多,這裏只帶大家打個,zabbix 是如何調用的:直接指明要調用 zabbix server 的哪個方法,要傳給這個方法的參數有哪些。

03、基於 zerorpc

以上介紹的兩種rpc遠程調用方式,如果你足夠細心,可以發現他們都是http+rpc 兩種協議結合實現的。

接下來,我們要介紹的這種(zerorpc),就不再使用走 http 了。

zerorpc 這個第三方庫,它是基於TCP協議、 ZeroMQ 和 MessagePack的,速度相對快,響應時間短,併發高。zerorpc 和 pyjsonrpc 一樣,需要額外安裝,雖然SimpleXMLRPCServer不需要額外安裝,但是SimpleXMLRPCServer性能相對差一些。

pip install zerorpc -i https://pypi.douban.com/simple

服務端代碼

import zerorpc

class caculate(object):
    def hello(self, name):
        return 'hello, {}'.format(name)

    def add(self, x, y):
        return x + y

    def multiply(self, x, y):
        return x * y

    def subtract(self, x, y):
        return abs(x-y)

    def divide(self, x, y):
        return x/y

s = zerorpc.Server(caculate())

s.bind("tcp://0.0.0.0:4242")
s.run()

客戶端

import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")

客戶端除了可以使用zerorpc框架實現代碼調用之外,它還支持使用“命令行”的方式調用。

客戶端可以使用命令行,那服務端是不是也可以呢?

是的,通過 Github 上的文檔幾個 demo 可以體驗到這個第三方庫做真的是優秀。

比如我們可以用下面這個命令,創建一個rpc server,後面這個 time Python 標準庫中的 time 模塊,zerorpc 會將 time 註冊綁定以供client調用。

zerorpc --server --bind tcp://127.0.0.1:1234 time

在客戶端,就可以用這條命令來遠程調用這個 time 函數。

zerorpc --client --connect tcp://127.0.0.1:1234 strftime %Y/%m/%d

03. 往rpc中引入消息中間件

經過了上面的學習,我們已經學會了如何使用多種方式實現rpc遠程調用。

通過對比,zerorpc 可以說是脫穎而出,一支獨秀。

但為何在 OpenStack 中,rpc client 不直接 rpc 調用 rpc server ,而是先把 rpc 調用請求發給 RabbitMQ ,再由訂閱者(rpc server)來取消息,最終實現遠程調用呢?

為此,我也做了一番思考:

OpenStack 組件繁多,在一個較大的集群內部每個組件內部通過rpc通信頻繁,如果都採用rpc直連調用的方式,連接數會非常地多,開銷大,若有些 server 是單線程的模式,超時會非常的嚴重。

OpenStack 是複雜的分佈式集群架構,會有多個 rpc server 同時工作,假設有 server01,server02,server03 三個server,當 rpc client 要發出rpc請求時,發給哪個好呢?這是問題一。

你可能會說輪循或者隨機,這樣對大家都公平。這樣的話還會引出另一個問題,倘若請求剛好發到server01,而server01剛好不湊巧,可能由於機器或者其他因為導致服務沒在工作,那這個rpc消息可就直接失敗了呀。要知道做為一個集群,高可用是基本要求,如果出現剛剛那樣的情況其實是很尷尬的。這是問題二。

集群有可能根據實際需要擴充節點數量,如果使用直接調用,耦合度太高,不利於部署和生產。這是問題三。

引入消息中間件,可以很好的解決這些問題。

解決問題一:消息只有一份,接收者由AMQP的負載算法決定,默認為在所有Receiver中均勻發送(round robin)。

解決問題二:有了消息中間件做緩衝站,client 可以任性隨意的發,server 都掛掉了?沒有關係,等 server 正常工作后,自己來消息中間件取就行了。

解決問題三:無論有多少節點,它們只要認識消息中間件這一个中介就足夠了。

04. 消息隊列你應該知道什麼?

由於後面,我將實例講解 OpenStack 中如何將 rpc 和 mq broker 結合使用。

而在此之前,你必須對消息隊列的一些基本知識有個概念。

首先,RPC只是定義了一個通信接口,其底層的實現可以各不相同,可以是 socket,也可以是今天要講的 AMQP。

AMQP(Advanced Message Queuing Protocol)是一種基於隊列的可靠消息服務協議,作為一種通信協議,AMQP同樣存在多個實現,如Apache Qpid,RabbitMQ等。

以下是 AMQP 中的幾個必知的概念:

  • Publisher:消息發布者

  • Receiver:消息接收者,在RabbitMQ中叫訂閱者:Subscriber。

  • Queue:用來保存消息的存儲空間,消息沒有被receiver前,保存在隊列中。

  • Exchange:用來接收Publisher發出的消息,根據Routing key 轉發消息到對應的Message Queue中,至於轉到哪個隊列里,這個路由算法又由exchange type決定的。

    exchange type:主要四種描述exchange的類型。

    direct:消息路由到滿足此條件的隊列中(queue,可以有多個): routing key = binding key

    topic:消息路由到滿足此條件的隊列中(queue,可以有多個):routing key 匹配 binding pattern. binding pattern是類似正則表達式的字符串,可以滿足複雜的路由條件。

    fanout:消息路由到多有綁定到該exchange的隊列中。

  • binding :binding是用來描述exchange和queue之間的關係的概念,一個exchang可以綁定多個隊列,這些關係由binding建立。前面說的binding key /binding pattern也是在binding中給出。

在網上找了個圖,可以很清晰地描述幾個名詞的關係。

關於AMQP,有幾下幾點值得注意:

  1. 每個receiver/subscriber 在接收消息前都需要創建binding。
  2. 一個隊列可以有多個receiver,隊列里的一個消息只能發給一個receiver。
  3. 一個消息可以被發送到一個隊列中,也可以被發送到多個多列中。多隊列情況下,一個消息可以被多個receiver收到並處理。Openstack RPC中這兩種情況都會用到。

05. OpenStack中如何使用RPC?

前面鋪墊了那麼久,終於到了講真實應用的場景。在生產中RPC是如何應用的呢?

其他模型我不太清楚,在 OpenStack 中的應用模型是這樣的

至於為什麼要如此設計,前面我已經給出了自己的觀點。

接下來,就是源碼解讀 OpenStack ,看看其是如何通過rpc進行遠程調用的。如若你對此沒有興趣(我知道很多人對此都沒有興趣,所以不浪費大家時間),可以直接跳過這一節,進入下一節。

目前Openstack中有兩種RPC實現,一種是在oslo messaging,一種是在openstack.common.rpc。

openstack.common.rpc是舊的實現,oslo messaging是對openstack.common.rpc的重構。openstack.common.rpc在每個項目中都存在一份拷貝,oslo messaging即將這些公共代碼抽取出來,形成一個新的項目。oslo messaging也對RPC API 進行了重新設計,對多種 transport 做了進一步封裝,底層也是用到了kombu這個AMQP庫。(注:Kombu 是Python中的messaging庫。Kombu旨在通過為AMQ協議提供慣用的高級接口,使Python中的消息傳遞盡可能簡單,併為常見的消息傳遞問題提供經過驗證和測試的解決方案。)

關於oslo_messaging庫,主要提供了兩種獨立的API:

  1. oslo.messaging.rpc(實現了客戶端-服務器遠程過程調用)
  2. oslo.messaging.notify(實現了事件的通知機制)

因為 notify 實現是太簡單了,所以這裏我就不多說了,如果有人想要看這方面內容,可以收藏我的博客(http://python-online.cn) ,我會更新補充 notify 的內容。

OpenStack RPC 模塊提供了 rpc.call,rpc.cast, rpc.fanout_cast 三種 RPC 調用方法,發送和接收 RPC 請求。

  • rpc.call 發送 RPC 同步請求並返回請求處理結果。
  • rpc.cast 發送 RPC 異步請求,與 rpc.call 不同之處在於,不需要請求處理結果的返回。
  • rpc.fanout_cast 用於發送 RPC 廣播信息無返回結果

rpc.call 和 rpc.rpc.cast 從實現代碼上看,他們的區別很小,就是call調用時候會帶有wait_for_reply=True參數,而cast不帶。

要了解 rpc 的調用機制呢,首先要知道 oslo_messaging 的幾個概念

  • transport:RPC功能的底層實現方法,這裡是rabbitmq的消息隊列的訪問路徑

    transport 就是定義你如何訪連接消息中間件,比如你使用的是 Rabbitmq,那在 nova.conf中應該有一行transport_url的配置,可以很清楚地看出指定了 rabbitmq 為消息中間件,並配置了連接rabbitmq的user,passwd,主機,端口。

    transport_url=rabbit://user:passwd@host:5672

    def get_transport(conf, url=None, allowed_remote_exmods=None):
        return _get_transport(conf, url, allowed_remote_exmods,
                              transport_cls=RPCTransport)
  • target:指定RPC topic交換機的匹配信息和綁定主機。

    target用來表述 RPC 服務器監聽topic,server名稱和server監聽的exchange,是否廣播fanout。

    class Target(object):
            def __init__(self, exchange=None, topic=None, namespace=None,
                     version=None, server=None, fanout=None,
                     legacy_namespaces=None):
            self.exchange = exchange
            self.topic = topic
            self.namespace = namespace
            self.version = version
            self.server = server
            self.fanout = fanout
            self.accepted_namespaces = [namespace] + (legacy_namespaces or [])

    rpc server 要獲取消息,需要定義target,就像一個門牌號一樣。

    rpc client 要發送消息,也需要有target,說明消息要發到哪去。

  • endpoints:是可供別人遠程調用的對象

    RPC服務器暴露出endpoint,每個 endpoint 包涵一系列的可被遠程客戶端通過 transport 調用的方法。直觀理解,可以參考nova-conductor創建rpc server的代碼,這邊的endpoints就是 nova/manager.py:ConductorManager()

  • dispatcher:分發器,這是 rpc server 才有的概念 只有通過它 server 端才知道接收到的rpc請求,要交給誰處理,怎麼處理?

    在client端,是這樣指定要調用哪個方法的。

    而在server端,是如何知道要執行這個方法的呢?這就是dispatcher 要乾的事,它從 endpoint 里找到這個方法,然後執行,最後返回。

  • Serializer:在 python 對象和message(notification) 之間數據做序列化或是反序列化的基類。

    主要方法有四個:

    1. deserialize_context(ctxt) :對字典變成 request contenxt.
    2. deserialize_entity(ctxt, entity) :對entity做反序列化,其中ctxt是已經deserialize過的,entity是要處理的。
    3. serialize_context(ctxt) :將Request context變成字典類型
    4. serialize_entity(ctxt, entity) :對entity做序列化,其中ctxt是已經deserialize過的,entity是要處理的。
  • executor:服務的運行方式,單線程或者多線程

    每個notification listener都和一個executor綁定,來控制收到的notification如何分配。默認情況下,使用的是blocking executor(具體特性參加executor一節)

    oslo_messaging.get_notification_listener(transport, targets, endpoints, executor=’blocking’, serializer=None, allow_requeue=False, pool=None)

rpc server 和rpc client 的四個重要方法

  1. reset():Reset service.
  2. start():該方法調用后,server開始poll,從transport中接收message,然後轉發給dispatcher.該message處理過程一直進行,直到stop方法被調用。executor決定server的IO處理策略。可能會是用一個新進程、新協程來做poll操作,或是直接簡單的在一個循環中註冊一個回調。同樣,executor也決定分配message的方式,是在一個新線程中dispatch或是….. *
  3. stop():當調用stop之後,新的message不會被處理。但是,server可能還在處理一些之前沒有處理完的message,並且底層driver資源也還一直沒有釋放。
  4. wait():在stop調用之後,可能還有message正在被處理,使用wait方法來阻塞當前進程,直到所有的message都處理完成。之後,底層的driver資源會釋放。

06. 模仿OpenStack寫rpc調用

模仿是一種很高效的學習方法,我這裏根據 OpenStack 的調用方式,抽取出核心內容,寫成一個簡單的 demo,有對 OpenStack 感興趣的可以了解一下,大部分人也可以直接跳過這章節

以下代碼不能直接運行,你還需要配置 rabbitmq 的連接方式,你可以寫在配置文件中,通過 get_transport 從cfg.CONF 中讀取,也可以直接將其寫成url的格式做成參數,傳給 get_transport 。

簡單的 rpc client

#coding=utf-8
import oslo_messaging
from oslo_config import cfg

# 創建 rpc client
transport = oslo_messaging.get_transport(cfg.CONF, url="")
target = oslo_messaging.Target(topic='test', version='2.0')
client = oslo_messaging.RPCClient(transport, target)

# rpc同步調用
client.call(ctxt, 'test', arg=arg)

簡單的 rpc server

#coding=utf-8
from oslo_config import cfg
import oslo_messaging
import time

# 定義endpoint類
class ServerControlEndpoint(object):
    target = oslo_messaging.Target(namespace='control',
                                   version='2.0')

    def __init__(self, server):
        self.server = server

    def stop(self, ctx):
        if self.server:
            self.server.stop()

            
class TestEndpoint(object):

    def test(self, ctx, arg):
        return arg

    
# 創建rpc server
transport = oslo_messaging.get_transport(cfg.CONF, url="")
target = oslo_messaging.Target(topic='test', server='server1')
endpoints = [
    ServerControlEndpoint(None),
    TestEndpoint(),
]
server = oslo_messaging.get_rpc_server(transport, target,endpoints,executor='blocking')
try:
    server.start()
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    print("Stopping server")

server.stop()
server.wait()

以上,就是本期推送的全部內容,周末两天沒有出門,都在寫這篇文章。真的快掏空了我自己,不過寫完后真的很暢快。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

前端必備性能知識 – http2.0

前端開發中,性能是一定繞不開的,今天就來說一下前後台通信中最重要的一個通道–HTTP2.0

 最開始的通訊協議叫http1.0,作為始祖級的它,定義了最基本的數據結構,請求頭和請求體,以及每一個字段的含義,它順應了當時的互聯網需求,首次實現了瀏覽器與後端的交互,但它有一個時代烙印,就是短連接,每次請求就會建立一個TCP連接,三次握手四次揮手,用完就關閉,假如瀏覽器有300個請求,那麼它就建立了300個連接,這樣就給服務端帶來的很大的壓力,即使它只是一個很小很小的請求,後來,大家發現這樣不行啊,內容一多,服務端就頂不住了,然後就開始想辦法擴展它,

這樣http1.1就出現了,建立了長連接,通過keepalived開啟連接復用,什麼意思?還拿這300個請求來說,瀏覽器默認一次支持6個請求,當這6個請求結束以後,會繼續復用這6個請求,每個請求都是異步的,不會讓這6個連接閑着,直到300個請求結束。

好像這樣就解決了問題,可是細想一下好像不對,硬件更新快,後端硬件性能提升很快,它可以支持很多線程進行計算,但瀏覽器還是6個,那豈不是白白浪費了硬件設備,所以http2.0就出來了,多路復用的單一長連接

什麼意思呢,看這個

 

 

http1.1中,當建立連接,並響應完以後,會繼續復用該條連接去請求資源,但這6條請求是不變的,只不過復用了而已,在2.0中就不一樣了,只用這一條長連接,請求多個資源,一下子就減少了5條連接(包括每次連接時的三次握手和四次揮手),還有tcp慢啟動帶來的網絡延時,而它之所以一個連接上能放這麼多內容,底層是由於它以數據幀的形式進行傳輸的,一個數據包中包含多個資源。

http頭部壓縮和緩存

我們在請求內容的時候,有時候會出現你請求的內容很少,但是請求頭字段的體積比內容的體積都大的情況,而且每次請求就帶着這個相同請求頭,一旦1萬個請求過來了,壓力就明顯了,下面是例子。

壓縮以後減少了一般的體積,而且它還會緩存請求頭,因為每次的請求頭都一樣,所以在底層,它將這個請求頭用一個符號比如1來表示去發請求,而後端也會去解析這個1進而進行處理,所以原來是一大段的字段內容,而現在就是一個符號就表示出來了。

兼容http1.1,基於https進行部署,服務端主動推送內容

如果發現瀏覽器不支持2.0,則自動向下兼容

部署升級,則如下

瀏覽器與nginx交互用https進行加密傳輸,反向代理nginx與服務器的comact是明文傳輸

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

[simple-orm-mybaits]基於Mybatis的ORM封裝介紹

目錄

  • 前言
  • ORM框架現狀
  • Mybatis優缺點
  • simple-orm-mybatis設計思路介紹
  • simple-orm-mybatis使用說明
  • simple-orm-mybatis實際使用
  • 推薦最佳實踐
  • 更多說明

simple-orm-mybatis在github開源,直接跳轉查看,歡迎大家fork和star。有什麼建議也可以提出,我會儘快修復或實現。

前言

最早接觸Java的web開發框架就是SSH,其中的H就是Hibernate。Hibernate作為最出名的Java的ORM框架,現在的版本已經到了5.3.10.Final,6.0.0.Alpha2。圍繞數據持久化或者DAL層也發展了多種工具,Hibernate Validator目前也是在很多的企業框架中被用作數據有效性檢查。
接下來還有用過JPA來實現ORM。JPA全稱Java Persistence API,是JSR-220(EJB3.0)規範的一部分,在JSR-220中規定實體對象(EntityBean)由JPA進行支持。在我的使用中,實際上底層實現仍然使用了Hibernate,只是標註或者操作類都是使用了JPA的接口標準而沒有直接使用Hibernate。
Hibernate具有強大的ORM封裝能力,極大的簡化了CUD的操作,而且無需做太多的配置,使用標準註解就能解決很多問題。簡單的查詢操作也不在話下,多級關聯、延遲查詢等豐富特性基本上也可以說是極大覆蓋了開發過程的各種需求。但是實際上在這麼多團隊中,很多人的反饋是這樣的:“Hibernate的ORM確實很方便,但是在一些特殊條件下很難用,比如複雜查詢就很難控制語句生成的效率”。Hibernate有三種查詢方式:HQL、Criteria、Native Sql。確實在複雜查詢下,如果使用Criteria,確實能夠拼出想要的語句,但是可能對於其中的方法要非常熟悉,學習曲線很高,沒辦法做到團隊中每個成員都能數量輕易的掌握,而且後期的審核很困難無法直接看清邏輯。HQL和NativeSql對我的感覺,似乎回到了JSP時代,HTML和Java代碼混寫,很難忍受。這時候大家找到了另外的框架Mybatis。

ORM框架現狀

截止(2019/5/27)Mybatis的Star是10806,UsedBy140381;Hibernate的Star是3817,UsedBy157879。看使用量Hibernate和Mybatis其實已經差不多了,實際企業開發中,Mybatis可能還會更多一些。

Mybatis優缺點

Mybatis放棄了Hibernate的強封裝,主要包含了映射的部分,而且放棄了自動解析生成Sql,而直接使用用戶XML配置Sql的方式,只是在Sql的拼接上提供了一些標籤來避免重複代碼。這樣的討巧之處在於:

  • 門檻降低:開發人員不需要了解框架的內部語法,只需要了解Sql語法即可。
  • 可讀性提高:審核代碼時,很容易的就能看清楚多級關聯以及關聯使用的條件、語法是否正確。
  • 維護方便:當查詢語法出錯時,Mybatis只需要修改XML,而Hibernate則需要修改代碼重新編譯,操作相對複雜。

缺點也有幾處:

  • 簡單操作複雜:由於放棄了自動解析生成Sql,所以普通的CUD也需要手動編寫Sql
  • 實體映射複雜:必須在XML文件中配置大量的字段映射
  • SqlMapper中的sql id風險:由於XML中的sql id是一個字符串,只能複製粘貼出來,所以出錯的幾率也比較大。

實際上,針對上面缺點,Mybatis也提供了解決方案。
先說sql id的,在新的Mybatis中,實際上已經採用了面向接口的編碼方式,如下面的例子:

接口類

public interface UserMapper {
    public User findUserById(Integer id);
}

mapper的xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.software5000.mapper.dao.UserMapper">
    <select id="findUserById" resultType="com.software5000.entity.User" parameterType="int" >
        select * from user where id =#{id}
    </select>
</mapper>

這樣就可以直接調用sql語句:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.findUserById(1);

再說映射複雜的,Mybatis提供了MyBatis Generator作為解決方案,一個命令生成下列內容:

  • 匹配表結構的Java POJOs。可能包括:
    • 表結構中主鍵字段(如果存在主鍵)
    • 表結構中的非主鍵字段(除了BLOB字段)
    • 表結構中的BLOB字段(如果有BLOB字段)
    • 動態查詢、更新和刪除的接口類

自動生成也能夠合理的生成類之間的繼承關係。注意生成器也能夠定義生產不同的POJO層次 – 例如如果你想要可以為每個表格生成一個單獨的領域對象。

  • Mybatis/iBATIS兼容的SQL MAP的XML文件。按照配置針對每個表生成簡單的增刪改查的SQL方法。生成的SQL包括:
    • 插入
    • 按主鍵更新
    • 使用example更新 (使用動態的where條件)
    • 按主鍵刪除
    • 使用example刪除 (使用動態的where條件)
    • 按主鍵查詢
    • 使用example查詢 (使用動態的where條件)
    • 使用example統計

生成的SQL語句取決於表格的結構(例如,表格如果沒有主鍵,就不會生成按主鍵更新的方法)

Java客戶端生成類能夠合理使用上面的對象。生成的Java客戶端類也是有選擇性的。

  • 使用Mybatis 3.x的會生成:
    • 一個mapper接口和XML中的語句id相同,用於直接調用。
  • 使用iBATIS 2.x的會生成:
    • 適用於Spring框架的DAO層
    • 使用IBATIS SQL映射API的DAO層。這些DAO可以使用兩種方式:使用構造函數提供SqlMapClient,或者通過注入方式提供。
    • DAOs that conform to the iBATIS DAO Framework (an optional part of iBATIS, this framework is now deprecated and we suggest that you use the Spring framework instead)
    • iBATIS的DAO框架符合的DAO層(iBATIS的一個額外部分,但是現在這個框架已經失效了,所以建議使用Spring DAO的框架代替)

但是,我對於MyBatis Generator的態度是堅定的反對。原因是我認為自動生成違反了簡單的原則,“如無必要,勿增實體”。自動生成的可復用性和可讀性一定是比較差的。我覺得最好的代碼就是不存在的代碼。因此我希望能夠有一個簡單框架在Mybatis之上,接入簡單無侵入,能夠提供基本的增刪改查方法。這就是下面的 simple-orm-mybatis 。

simple-orm-mybatis設計思路介紹

simple-orm-mybatis設計的初衷就是希望提供一個簡單無侵入,而且無需編寫或者生成任何代碼就可以使用直接操作對象的方式來進行增刪改查的操作。要實現這樣的要求,主要是兩個主要技術點:

  1. 利用反射機制對應實體對象與數據庫結構
  2. 解析對象並且生成對應操作的SQL語句

第一點核心就是反射,設計要點如下:

  • Java對象與數據庫結構的對應規則
  • 虛字段(無數據庫對應字段)的處理
  • 考慮多層繼承的對象解析
  • 值的設置與獲取方式

第二點核心在於SQL解析,設計要點如下:

  • 根據不同入口區分基本CRUD語法結構
  • 字段(列名)需要分為值字段與查詢字段兩類
  • 更新操作的時候,Null,空,有值的區分
  • 支持多種匹配操作符(大於、小於、Like等)

simple-orm-mybatis使用說明

  1. 首先引入依賴,項目已經發布在Maven Central上,可以直接引入。
<dependency>
    <groupId>com.software5000</groupId>
    <artifactId>simple-orm-mybatis</artifactId>
    <version>1.0.2</version>
</dependency>
  1. 接着需要將 BaseDaoMapper.xml文件放在你的 mapper 的掃描文件夾內。

  2. 最後需要在你的代碼中添加一個 BaseDao 實現類,用於提供數據庫操作服務(注意:需要在spring的掃描包內,因為需要注入某些屬性),整個複製即可,類名可以改為你自己需要的名字
import com.software5000.base.BaseDao;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 整個類以 <code>org.mybatis:mybatis-spring:2.0.0</code> 的 <code>org.mybatis.spring.supportSqlSessionDaoSupport</code>
 * 為參考編寫而成
 */
@Component
public class MyBaseDao extends BaseDao {
    private SqlSessionTemplate sqlSessionTemplate;
    
    public MyBaseDao() {
            // 在默認構造函數中設置 數據庫是否蛇形, 數據庫格式大小寫, 通用忽略的字段名稱
            this.initConfig(true,false,"serialVersionUID");
    }
        
    @Resource
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
            this.sqlSessionTemplate = this.createSqlSessionTemplate(sqlSessionFactory);
        }
    }

    protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Override
    public SqlSession getSqlSession() {
        return this.sqlSessionTemplate;
    }
}

simple-orm-mybatis實際使用

這裏給了一個單元測試的例子,實際一般是在service層直接使用,無需添加任何代碼。
示例中的 DailyRecord是一個普通實體類,沒有任何繼承。

// 獲取啟動類,加載配置,確定裝載 Spring 程序的裝載方法,它回去尋找 主配置啟動類(被 @SpringBootApplication 註解的)
@SpringBootTest
// 讓 JUnit 運行 Spring 的測試環境, 獲得 Spring 環境的上下文的支持
@RunWith(SpringRunner.class)
public class BaseDaoTest {

    @Autowired
    private MyBaseDao myBaseDao;

    @Test
    public void testBaseDao(){
        DailyRecord dailyRecord = new DailyRecord();
        List<DailyRecord> dailyRecords = myBaseDao.selectEntity(dailyRecord);
        System.out.println(dailyRecords.size());
    }
}

推薦最佳實踐

雖說整體的設計基於無侵入,基本沒有任何前提,但是還是有一些推薦的實踐希望大家能夠去嘗試:

1、 數據結構設計建議包含int類型的自增主鍵設計,名稱叫id。
原因:很多時候我們的業務主鍵是有一套特定規則,但是很有可能面對修改,所以底層關聯主鍵統一使用id關聯在後期面對修改時影響較小。
弊端:
1. 如mysql中int最長2147483647,考慮到自增id可能會有跳過不連續的情況,需要考慮實際可用的存儲量。不過大部分的業務表是到不了這個數量級的。
2. mysql的InnoDB有自增主鍵鎖表的問題,超大併發插入可能會影響效率。不過在5.1.22有提供了改進的策略。

2、 數據結構與實體結構盡量符合統一轉換規則。
原因:這樣研發過程中,實體與數據庫的映射會比較簡單,無需大量的自定義。建議的規則有三類:
– 兩邊都使用駝峰。
– 實體使用駝峰,數據庫使用全小寫蛇形
– 實體使用駝峰,數據庫使用全大寫蛇形

3、 代碼中實體的字段類型使用封裝類型而不是基本類型
原因:基本類型是有默認值存在,而數據庫中我們一旦設置字段可空,就會有NULL值存在。所以建議全部使用封裝類型。下面附上各種基本類型的默認值

基本類型 默認值
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0d
char ‘\u0000’
boolean false

4、 分頁算法
原因:分頁推薦使用PageHelper,是利用Mybatis的底層插件機制修改Sql語句,也是無侵入。

更多說明

更多說明,可以參考github上的wiki頁面

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

超級簡單的跨平台高性能音視頻播放框架QtAv編譯指南

目錄

  • 一、了解QtAv
  • 二、相關文章
  • 三、下載QtAv源碼
  • 四、下載QtAv依賴庫
  • 五、設置環境變量
    • 1、gcc設置方式
    • 2、msvc(cl)設置方式
  • 六、編譯
  • 七、測試

一、了解QtAv

這幾天抱着試一試的心態,嘗試着了解了下QtAv這個庫,感覺確實挺不錯的,因此就打算學習下這個庫。

斷斷續續的看了不少文章,大多數都是通過百度搜索出來的文章。說實話百度上大多數文章內容都差不多,而且很少有文章說清楚了編譯時的環境配置和編譯器上的區別,導致我自己也一度認為這個庫很難編譯。其實真的不難

網上的資源真的很多,但是有點兒雜亂,新手上來一看可能很容易就懵逼了。可是我這裏要告訴大家,真的不需要害怕,這個庫的編譯真的炒雞簡單,不信看我第三小節開始的編譯步驟,簡單到不敢相信。

因為我看到了windows編譯qtav這篇文章,文章中清楚的說明了環境變量配置是需要根據編譯器進行選擇設置的,這時自己的思路也一下子就開闊了。

我這裏使用的是QtCreator編輯器,編譯器使用的是是MSVC,是vs2013的編譯器。所以頭文件需要配置到Include上,庫文件需要配置到Lib目錄上。

如果是gcc的編譯器,配置才可能像下邊這樣。這個我沒有測試,因為我自己是msvc環境,不過網上這麼多人說了,估計應該也沒啥問題。這也是為啥我開頭說網上資源亂,因為我看的大多數是Mingw集成環境下的文章。

CPATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\include
LIBRARY_PATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\lib

首先說明我的編譯環境:

  • Qt版本:Qt5.7.1
  • 編譯器:vs2013上的MSVC編譯器
  • 編輯工具:QtCreator 4.2,其實跟這個關係不大,只是一個ide而已,我們使用的編譯器仍然是微軟的msvc編譯器。
  • 系統:Win10 64位

重點強調下,windows編譯qtav這篇文章一定要看,內容真的很實用。主要是告訴你在編譯前期,msvc和gcc兩種編譯器下,怎麼去配置環境變量。

二、相關文章

編譯步驟:Qt5.5.0編譯QtAV

不同編譯器下環境變量配置:windows編譯qtav

我自己是看着Qt5.5.0編譯QtAV這篇文章進行編譯的,最起碼資源都是在文章中的連接里下載的,包括QtAv源碼和依賴庫QtAV-depends-windows-x86+x64

但是參考這篇文章中配置環境變量時,一定要注意,這篇文章中的作者是GCC編譯器。而我們自己去要根據自己的編譯環境來配置環境變量,如果你是MINGW集成環境,也就是說你是GCC編譯器,那麼恭喜你,直接按原文配置即可。

但是,如果你不是GCC編譯器,那麼你就需要看windows編譯qtav
這篇文章,他會告訴你,其他編譯器怎麼配置環境變量

MSVC編譯器,配置方法如下。把頭文件和庫文件分別配置在Include和LIB目錄上。
如果是gcc的編譯器,需要把頭文件和庫文件分別配置在CPATH和LIBTRARY_PATH環境變量上。

三、下載QtAv源碼

源碼下載時直接上官方的github即可,QtAv

四、下載QtAv依賴庫

由於QtAv是基於ffmpeg開發的,因此我們需要下載相關依賴庫。QtAV-depends-windows-x86+x64

五、設置環境變量

根據不同編譯器設置方法不一樣,具體參看windows編譯qtav這篇文章

1、gcc設置方式

CPATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\include
LIBRARY_PATH : C:\Users\Administrator\Desktop\QtAV-depends-windows-x86+x64\QtAV-depends-windows-x86+x64\lib

2、msvc(cl)設置方式

圖中環境變量列表中加粗的字即是我添加的環境變量,msvc編譯器下INCLUDE和LIB這個兩個變量本身就是存在的,所以我們只需要在值那一列把include和lib目錄添加上即可。

注意:需要添加自己的QtAV-depends-windows-x86+x64依賴庫路徑

六、編譯

環境變量配置好之後,直接點擊構建即可,編譯成功后,效果如下

七、測試

編譯完成之後,我們會發現bin目錄下會有很多可執行文件,這個時候我們可以執行其中某一個可執行文件對我們編譯的庫進行測試。

首先拷貝QtAv的依賴庫ffmpeg,找到之前解壓的QtAV-depends-windows-x86+x64文件夾,把裡邊的bin目錄下的資源文件都拷貝到我們剛才編譯出來的QtAv目錄下。

找到我們剛才編譯生成的bin目錄,打開裡邊的simpleplayer.exe可執行程序。選擇一個本地的資源文件進行播放,效果圖可能如下圖所示,這裡是只放了一張圖,主要作為示意。

到這裏,我們的QtAv就編譯完成了。

後續有時間我會嘗試使用這個庫,然後做更進一步的分析

很重要–轉載聲明

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

談談實現瀑布流布局的幾種思路

最近遇到這麼一個需求,需要在手機上做一個兩列的瀑布流布局,後來就把這個問題研究了一下,做個記錄。

一般來講,這種布局可以分為兩種情況:

  1. 圖片的數量是一定的,不需要頁面滾動到底部時,再動態加載圖片,只需要將圖片排成若干列

  2. 圖片的數量的不定的,頁面觸底時,需要從遠程加載圖片。

前者使用css的方法即可解決,後者則需要js來幫忙。

css解法

一、CSS多列布局

當我們展示的圖片數量一定時,可以優先採用css解法。其中一種方法是藉助css的多欄布局:


.photos{
  
  column-count: 3;
  column-gap: 10px;
}

得到的效果如下:

&lt;p&gt;See the Pen &lt;a href=’https://codepen.io/imgss/pen/Kjmjmq/’&gt;Kjmjmq&lt;/a&gt; by imgss&lt;br /&gt; (&lt;a href=’https://codepen.io/imgss’&gt;@imgss&lt;/a&gt;) on &lt;a href=’https://codepen.io’&gt;CodePen&lt;/a&gt;.&lt;br /&gt;

二、flex布局

flex布局同樣可以做到這一點,訣竅在於將flex-direction設為column;但是相對於多列布局,需要根據瀑布流的列數,計算一個合適的容器高度,不然可能會導致多出一行。如果你在下面的demo 中,看到了4列,不要懷疑,就是我計算的容器高度不合適導致的。。。

&lt;p&gt;See the Pen &lt;a href=’https://codepen.io/imgss/pen/XLRvmQ/’&gt;waterflow-flex&lt;/a&gt; by imgss&lt;br /&gt; (&lt;a href=’https://codepen.io/imgss’&gt;@imgss&lt;/a&gt;) on &lt;a href=’https://codepen.io’&gt;CodePen&lt;/a&gt;.&lt;br /&gt;

js解法

當圖片需要動態插入時,上面的兩種方法就不合適了,因為他們本質上是將圖片按照縱向進行排列的。圖片動態插入時通常我們希望圖片是按橫向插入到容器中的。這時候就需要js來幫忙了。首先,我們看看瀑布流和背包問題的關係。

瀑布流的基本思路是將一堆圖片放到若干列中,列與列之間的高度比較均勻,而不會相差太大。假如我們要分成兩列,那麼,問題就變成了,從 n 張圖片中挑出 m 張,使這 m 張圖片的總高度盡量接近 n 張圖片總高度的 1 / 2。於是這就變成了一個背包問題

01背包問題

背包問題是啥這裏不做展開,說白了是將一個複雜的問題分解為幾個簡單的問題,大佬們講的都比我好,網上也有各個語言版本的實現,不太了解的同學可以查看上面的鏈接。這裏直接放一個函數


function dp(ws, vs, limit) {
    let len = ws.length;
    let tables = new Array(len).fill().map(x => [])
    tables[-1] = new Array(limit + 1).fill(0);

    for(let i = 0; i < len; i++) {
        for (let w = 0; w <= limit; w++) {
            if (ws[i] > w) {
                tables[i][w] = tables[i-1][w]
            } else {
                tables[i][w] = Math.max(tables[i-1][w], tables[i-1][w-ws[i]] + vs[i])
            }
        }
    }
    // 回溯得到應該選哪些
    let max = limit;
    let selected = [];
    for (let idx = len - 1; idx >= 0; idx--) {
        if (ws[idx] <= max) {
            let isSelected = tables[idx-1][max] < tables[idx-1][max-ws[idx]] + vs[idx]
            if(isSelected) {
                selected.push(idx);
                max = max - ws[idx];
            }
        }
    }
    return selected;
}

有了這個解法之後,我們也就不難寫出一個瀑布流布局。具體思路是:假設我們要做一個3列的瀑布流布局,那麼可以不斷從圖片數組中選出一組圖片,使圖片的高度接近總高度的1/3,最終得到3組圖片。下面是一個代碼片段

      // colCount 表示要生成幾列
      while(colCount--) {
          // 獲取被選出的照片索引
          let idxs = dp(photoHeights, photoHeights, aver)
          // 得到被選出的一組圖片
          let photoCol = photos.filter((p,idx) => idxs.includes(idx)) 
          this.cols.push(photoCol)
          photoHeights.forEach((v,i) => {
          if (idxs.includes(i)) {
            photoHeights[i] = null
          }
        })
      }

下面這個demo就是按上面的思路實現的,可以拖動下面的滑塊來改變列數,觀察底部的間隙。在使用背包算法解決瀑布流問題時,一個需要我們注意的地方是,要將圖片高度轉化成整數。

&lt;p&gt;See the Pen &lt;a href=’https://codepen.io/imgss/pen/agwbgx/’&gt;waterflow-js&lt;/a&gt; by imgss&lt;br /&gt; (&lt;a href=’https://codepen.io/imgss’&gt;@imgss&lt;/a&gt;) on &lt;a href=’https://codepen.io’&gt;CodePen&lt;/a&gt;.&lt;br /&gt;

參考文章:https://www.cnblogs.com/ainyi/p/8766281.html
https://www.cnblogs.com/AndyJee/p/4543424.html
https://segmentfault.com/a/1190000012829866

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

機器學習實記(一)簡介機器學習

一.寫在前面

  本系列中的內容來自對李宏毅老師的機器學習教程的理解和一些個人理解的筆記,主要用於之後方便查看複習。李宏毅老師的機器學習教程地址:https://www.bilibili.com/video/av35932863?from=search&seid=3902369523652897681。

二.機器學習概念

2.1人工智能、機器學習、深度學習這幾個概念的關係

  要想理解清楚機器學習的基本概念,首先要明白機器學習在人工智能領域中所處的位置,再簡單明了些首先要明白人工智能、機器學習、深度學習這幾個概念的關係。三者關係如下圖2-1

                      圖2-1 人工智能、機器學習、深度學習三者關係

從圖可以直觀的看出,人工智能的範圍>機器學習的範圍>深度學習的範圍,而平時常說的神經網絡的這些有關概念則是屬於深度學習下的分支。再具體而言這三者的關係,其實人工智能是作為我們最終所要達到的目標,所謂的人工智能概念我覺的用之前某科學家提過的一種說法簡單理解一下就行,即當用一塊黑幕將計算機遮蓋起來,黑幕外的人無法區分所交互的對象是人還是計算機就算是達到了人工智能標準,再簡單點說就是使計算機做到過去只有人才能做到的事,其他更為深入的解釋歡迎查看百度詞條https://baike.baidu.com/item/人工智能/9180?fr=aladdin。而機器學習則是實現人工智能這一目標的手段,這說明還有其他手段有興趣的同學可以自行去了解一下,不過個人認為其餘的手段不是現在的主流研究方向吧。而深度學習則是作為機器學習下的一個重要分支。

2.2 理解機器學習

  所謂機器學習概念,簡單的理解就是教會計算機學習,舉個具體的例子,當你給它看完貓的某一張圖片后,你告訴機器這是貓,當你給它看完一張狗的圖片后,你告訴它這是狗,不斷重複這一過程讓機器學習大量不同的貓狗照片,學習結束后給予機器一張全新的貓或狗的照片,機器能夠成功的識別出貓和狗。再進一步抽象的理解,我們可以發現這一過程十分類似於數學上求解函數表達式,即這個過程其實是在尋找一個函數f,當輸入某張貓的照片函數f的輸出為貓,當輸入某張狗的照片函數f的輸出為狗。再進一步具體而言整個機器學習框架可以理解為如圖2-2

                                                                            圖2-2 機器學習框架描述

整個機器學習框架的步驟大體上分為三步:第一步定義一個函數池,其中有大量的備選函數f1、f2、f3…..fn;第二步對各個函數進行評價,這裏的話其實我們可以將每個函數理解為一個模型,所謂的評價的標準可以理解為對每個輸入照片各個模型所能識別出來的精確度,優秀的模型識別照片的準確度更高;第三部選出最優的函數fbest,即選出最優的模型。

2.3 理解機器學習的學習圖

  機器學習的學習圖如圖2-3,這張圖看起來很複雜,其實結構很清晰,主要分為三中顏色藍、橙、綠三種顏色,分別代表運用情景,所要解決問題的目標,以及解決問題用到的方法。

                           圖2-3 機器學習學習圖

  首先是藍色部分,即運用情景從圖中可以看出有supervise learning、semi-supervise learning、transer learning、unsupervised learning、reinforcement learning,看到這麼多運用情景不經想問一個問題,為什麼會有這麼多運用場景的劃分?原因其實很簡單,從前面的對機器學習的介紹中我們可以發現,機器學習的過程中是需要大量的帶標籤的數據,所謂帶標籤的數據就是指給出了輸入數據的正確結果,比如說輸入一張貓的圖片,這個貓就是這張圖片的標籤,但其實當數據量巨大的時候,給每個數據標上標籤是需要消耗大量時間的,所以由輸入的數據是否帶有標籤就產生了不同的運用場景。

  supervise learning 即監督學習,訓練所使用的數據均帶標籤,即告訴機器輸入數據的正確答案。semi-supervise learning 即半監督學習,訓練中的數據有一部分帶標籤,其餘數據不帶標籤交給機器自己學習。transer learning 即遷移學習 訓練數據中有一部分帶標籤,剩下的數據來自其他模型帶標籤或不帶標籤的數據,舉個例子我們做貓和狗的分類,我們有少量的已經有標籤的貓和狗的數據,還有大量的可能是帶標籤或不帶標籤的其他動物的數據,將這些數據也交由機器學習。unsupervised learning 即無監督學習,訓練中的數據均不帶標籤。reinforcement learning 即強化學習,這種學習方式與前面的學習方式均有所不同,前面的學習方式中均為告訴機器輸入數據的正確答案或者直接將數據交由機器,在強化學習中數據均不帶標籤,它是通評價告訴機器這次學習的結果,舉個例子阿爾法狗,當它完成一次棋局對戰最終取得勝利,則給與機器較高的評價,讓機器自身進行調整,雖然機器本身並不知道自己到底哪下的好,但知道這麼下贏了。

  其次是橙色部分,即所要解決問題的目標,分為regression和classification以及structured learning。regression即所要解決的問題的解是個數值,比方說預測某日pm2.5的值;classification即所要解決的問題為分類問題,包括二分類即回答是或者否,比方說判斷郵件是否為垃圾郵件,多分類對輸入的數據進行多個類別分類,比方說判斷輸入的文章屬於哪個板塊是娛樂版塊還是體育板塊還是金融板塊等等;structured learning即所要解決的問題是結構化的,比方說輸入一段語言判斷語言的內容。

  最後是綠色部分,這一塊的話還記的我在2.2中說過的函數池嗎,所謂的函數其實就是模型,我們可以使用不同的模型來解決不同的問題,這些模型有linear model即線性模型、non-linear model 即非線性模型,非線性模型中就包括之前提到的深度學習還有一些非線性模型。

三.寫在最後

  這一部分主要是對機器學習要有個整體的大概理解,包括理解機器學習的整體框架的大致模樣,尤其是要明白框架中所說的函數其實就是模型的概念,再由就是要對為何要這樣劃分學習圖有自己的理解,至於是否要記住學習圖的內容倒是其次。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

【工具篇】在.Net中實現HTML生成圖片或PDF的幾種方式

前段時間由於項目上的需求,要在.Net平台下實現把HTML內容生成圖片或PDF文件的功能,特意在網上研究了幾種方案,這裏記錄一下以備日後再次使用。當時想着找一種開發部署都比較清爽並且運行穩定的方案,但實際上兩者同時滿足基本不可能,只能做一個自己覺得合適的取捨,下面從兩個維度(清爽指數和功能指數)逐一對比。


1.   WebBrowser

這種方案在開發時不依賴任務外部程序集和nuget包,部署時也不需要安裝額外的工具和服務,可以說是非常清爽了。它藉助了WinForm下的WebBrowser控件實現HTML內容渲染,並把渲染結果繪製在Bitmap中,進而保存成圖片或PDF文件。這種方案簡單粗暴,是C#中最基礎的實現方式,也是網上搜索結果最多的一種,下面看它的核心代碼(從網上拼湊得來):

 1     class WebBrowserPage2Image
 2     {
 3         Bitmap m_Bitmap;
 4 
 5         string m_Url;               
 6 
 7         public void Convert(string pageUrl, string fileName)
 8         {
 9             m_Url = pageUrl;
10             Thread m_thread = new Thread(new ThreadStart(HtmlDrawToBitmap));
11             m_thread.SetApartmentState(ApartmentState.STA);
12             m_thread.IsBackground = true;
13             m_thread.Start();
14             m_thread.Join();
15             MemoryStream stream = new MemoryStream();
16             m_Bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
17             byte[] buff = stream.ToArray();
18             FileStream fs = new FileStream(fileName, FileMode.Create);
19             stream.WriteTo(fs);
20             stream.Dispose();
21             stream.Close();
22             fs.Close();
23         }
24 
25         private void HtmlDrawToBitmap()
26         {
27             WebBrowser browser = new WebBrowser();
28             browser.ScrollBarsEnabled = false;
29             browser.Navigate(m_Url);
30             browser.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(delegate (object sender, WebBrowserDocumentCompletedEventArgs bdce)
31             {
32                 if (browser.ReadyState == WebBrowserReadyState.Complete)
33                 {
34                     //myWebBrowser.Document.Body.Style = "zoom:180%";
35                     Rectangle r = browser.Document.Body.ScrollRectangle;
36                     browser.Height = r.Height;
37                     browser.Width = r.Width;
38                     m_Bitmap = new Bitmap(browser.Width, browser.Height);
39                     browser.BringToFront();
40                     browser.DrawToBitmap(m_Bitmap, new Rectangle() { Width = browser.Width, Height = browser.Height });
41                 }
42             });
43             while (browser.ReadyState != WebBrowserReadyState.Complete)
44             {
45                 Application.DoEvents();
46             }
47             browser.Dispose();
48         }
49     }

View Code

雖然開發起來非常簡潔,但是問題也很明顯。WebBrowser是Winform下的一個組件,在非Winform項目中運行會出現不可知的異常,即使在Winform項目中,數據量比較大的時候依然會出現卡死的情況。我做過500次循環的測試,在執行到100多次的時候程序出現假死不動也無異常拋出。除此之外,生成的圖片失真也比較嚴重,特殊字體和部分CSS樣式無法渲染。總的來說,基本無法達到生成環境需求。

清爽指數:★★★★★    功能指數:★


2.         Wkhtmltox

這也是網上廣泛流傳的一個方案,wkhtmltox是一套開源的命令行工具,提供了圖片和PDF的轉換能力,它採用C++編寫,使用Webkit作為渲染引擎,開源地址是https://github.com/wkhtmltopdf/wkhtmltopdf。使用方法就是在命令行工具中執行命令,例如:

wkhtmltopdf --grayscale  https://www.baidu.com  baidu.pdf

如果要在.Net項目中使用的話,核心問題就是用程序喚起命令行,同時指定參數執行即可,類似於下面的代碼:

       System.Diagnostics.ProcessStartInfo Info = new System.Diagnostics.ProcessStartInfo();
       Info.FileName = @"D:\dev\wkhtmltox\bin\wkhtmltopdf.exe";
       Info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
       Info.CreateNoWindow = true;
       Info.Arguments = @"-q --orientation Landscape https://www.baidu.com D:\\baidu.pdf";
       System.Diagnostics.Process proc = System.Diagnostics.Process.Start(Info);
       proc.WaitForExit();
       proc.Close();

更多強大的功能例如加水印、分頁、改樣式等可以參考這篇文章:https://www.cnblogs.com/82xb/p/7837597.html

詳細的參數說明可以查看文檔:https://wkhtmltopdf.org/usage/wkhtmltopdf.txt

GitHub上有很多針對各個開發語言的封裝,使用起來比較方便,唯一不爽的是部署項目前要先安裝好這個工具。

清爽指數:★★★★    功能指數:★★★★


3.         PuppeteerSharp

這個就更厲害了,說到這個就不得不先介紹下Puppeteer,因為PuppeteerSharp正是從Puppeteer衍生而來。

Puppeteer是由谷歌開源的一個Node項目,它提供了和Chrome DevTools的通信能力,基本上我們能在Chrome實現的操作通過它的API都可以實現,強大到讓你不敢相信。主要的應用有:

  • 生成頁面快照(圖片、PDF)
  • 爬蟲,網站內容抓取
  • 自動化測試(模擬鍵盤鼠標輸入,表單提交,UI測試等)
  • 網站性能分析(追蹤,時間線捕獲等)

開源地址是https://github.com/GoogleChrome/puppeteer

在Node項目中使用Puppeteer非常簡單,先安裝npm包:

npm i puppeteer

安裝過程可能會有點慢,因為在安裝的時候會下載一個最近版本的Chromium(Mac下大概170M,Linux下大概282M,Windows下大概280M)。當然,如果你本地已經有一個Chromium,可以設置npm的全局配置PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 跳過下載,然後在程序中手動指定Chromium的位置。

生成圖片和PDF文件例子:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.baidu.com');
  await page.screenshot({path: 'baidu.png'});
  await page.pdf({path: 'baidu.pdf', format: 'A4'});
  await browser.close();
})(); 

Puppeteer默認使用無界面模式(headless:true),如果想看到完整的瀏覽器界面,可以通過下面的設置開啟:

  const browser = await puppeteer.launch({headless: false});

Puppeteer提供了豐富的選擇器接口,可以輕鬆實現模擬輸入和鼠標點擊,例如:

  await page.type('#index-kw', 'cnblogs');
  await page.click('#index-bn');

      還支持指定使用設備:

  const devices = require('puppeteer/DeviceDescriptors');
  await page.emulate(devices['iPhone 8']);

      詳細的API文檔可以參考:https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md

Puppeteer確實非常強大,但由於它是一個Node包無法直接在C#項目中使用,那怎麼辦呢?好在有國外的大神把Puppeteer移植到了.Net平台,也就是PuppeteerSharp。

注意:PuppeteerSharp是基於NetStandard 2.0開發的,所以項目的平台最低版本要是.NET Framework 4.6.1和.NET Core 2.0。

首先通過nuget安裝:

PM > Install-Package PuppeteerSharp

導入命名空間:

  using PuppeteerSharp;

下面是我在ASP.NET Core 2.1下封裝的測試方法:

        [HttpPost, Route("page2img")]
        public async Task<string> PageToImage(string url, int? width, int? height)
        {
            await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);
            var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true,
                //ExecutablePath="",
                Args = new string[] { "--no-sandbox" }
            });
            var page = await browser.NewPageAsync();
            bool fullPage = true;
            if (width.HasValue && height.HasValue)
            {
                await page.SetViewportAsync(new ViewPortOptions
                {
                    Width = width.Value,
                    Height = height.Value
                });
                fullPage = false;
            }
            await page.GoToAsync(System.Web.HttpUtility.UrlDecode(url));
            string fileName = $"/Files/{Guid.NewGuid().ToString()}.png";
            await page.ScreenshotAsync($"{AppDomain.CurrentDomain.BaseDirectory}{fileName}", new ScreenshotOptions { FullPage = fullPage });
            return $"{Request.Host.ToString()}{fileName}";
        }

上面方法的第一行:

  await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);

程序會判斷本地環境有沒有可用的Chromium,如果沒有的話會自動下載一個默認版本的Chromium,這個過程可能會有點長,下載成功後會在項目根目錄多一個這樣的文件夾:

和前面說的一樣,如果本地已經下載過Chromium,可以通過LaunchOptionsExecutablePath字段指定一個目錄。目前PuppeteerSharp在網上的資料還不是很多,但是得益於它與Puppeteer高度完整和相似的API,Puppeteer的文檔對它基本都能適用。

總體來說,這個工具功能強大並且比較穩定(我在Windows和Linux下都測試通過),是一個不錯的選擇,但是由於它必須依賴於Chromium來運行,打包部署並不是很方便,我建議把它作為一個獨立的web服務。

清爽指數:★★★    功能指數:★★★★★


4.         IronPdf

    除了一些開源的項目和工具能提供HTML轉圖片或PDF的功能,很多商業軟件公司也提供了這樣的產品,IronPdf算是裏面比較有代表性的一個。和其他收費軟件不同的是,IronPdf有一個對開發者免費試用的license:

    IronPdf的主要特性包括:

  • 任何類型的HTML文件、代碼片段、URL生成PDF
  • PDF編輯
  • 圖片與PDF互轉
  • 支持HTML5和CSS3,支持響應式布局,支持JS腳本,豐富的配置選項
  • 支持C#、VB、Webform、ASP.NET MVC、.NET CORE

    我們可以在官網下載DLL文件直接引用到項目,也可以通過nuget來安裝:

PM > Install-Package IronPdf

    導入命名空間:

  using IronPdf;

    一個最簡單的例子:

// Create a PDF from any existing web page
var Renderer = new IronPdf.HtmlToPdf();
Renderer.PrintOptions.EnableJavaScript = true;
Renderer.PrintOptions.PaperOrientation = IronPdf.PdfPrintOptions.PdfPaperOrientation.Landscape;
var PDF = Renderer.RenderUrlAsPdf("https://www.baidu.com");
PDF.SaveAs("baidu.pdf");

// This neat trick opens our PDF file so we can see the result
System.Diagnostics.Process.Start("baidu.pdf");

    添加水印:

  pdf.WatermarkAllPages("<h2 style='color:red'>SAMPLE</h2>", PdfDocument.WaterMarkLocation.MiddleCenter, 50, -45, "https://www.baidu.com");

    用圖片生成PDF文檔:

// Select one or more images.  This example selects all JPEG images in a specific folder.
var ImageFiles = Directory.EnumerateFiles(@"C:\project\assets").Where(f => f.EndsWith(".jpg") || f.EndsWith(".jpeg"));

// Convert the images to a PDF and save it.
ImageToPdfConverter.ImageToPdf(ImageFiles).SaveAs(@"C:\project\composite.pdf");

    更多高級功能和配置可以參考官網例子:https://ironpdf.com/examples/image-to-pdf/

 清爽指數:★★★★    功能指數:★★★★

    

寫在最後

    以上幾種方式,都是我在本次實踐中總結出來的,可能不是很全面,歡迎大家不吝補充。

    遺憾的是,最終項目沒有用上面的任何一種方式,而是抓取到HTML內容後用正則解析,然後用Bitmap一點一點重新畫圖生成圖片文件保存。因為我要截取的頁面內容很少,就是一個簡單的电子處方箋,需求上也沒有要求必須完全和原網頁100%一致,繪圖也算是一個不錯的方案,但是缺點是一旦HTML結構或樣式發生變化,那這套東西就失效了,好在這個不會輕易變更,也算是一個折中方案。

 

 

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

【劍指Offer】調整數組順序使奇數位於偶數前面

題目描述

輸入一個整數數組,實現一個函數來調整該數組中数字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。

解法1

最直接的思路是再構建一個新數組,先遍歷一遍原數組,把其中的奇數依次添加到新數組中,再遍歷一遍原數組把其中的偶數依次添加到新數組中,時間複雜度為O(2n)。實現代碼如下

實現代碼

public int[] reOrderArray2(int[] array)
{
    int[] arr = new int[array.Length];
    int index = 0;
    for (int i = 0; i < array.Length; i++)
    {
        // 奇數
        if ((array[i] % 2) != 0)
        {
            arr[index] = array[i];
            index++;
        }
    }
    for (int i = 0; i < array.Length; i++)
    {
        // 偶數
        if ((array[i] % 2) == 0)
        {
            arr[index] = array[i];
            index++;
        }
    }
    return arr;
}

解法2

C#的數組是不支持動態添加元素的,我們可以使用泛型List,來實現在指定位置插入元素。基本思路是遍歷原數組,依次將元素插入到List中,如果是偶數元素,默認插入到List的末尾。如果是奇數元素,則插入到所有的偶數元素之前(已插入的所有奇數元素之後),因此需要記錄最後插入的奇數元素的索引。實現代碼如下,算法的時間複雜度是O(n)

實現代碼

public int[] reOrderArray(int[] array)
{
    List<int> list = new List<int>();
    // 最後插入奇數元素的索引
    int index = 0;
    foreach (int i in array)
    {
        if ((i % 2) == 0)
            list.Add(i);
        else
        {
            list.Insert(index, i);
            index++;
        }
    }
    return list.ToArray();
}

解法3

上面的兩種解法都用到臨時數組或List,空間複雜度是O(n),某些情況下可能希望空間複雜度越低越好。下面這種解法雖然時間複雜度提高了,但降低了空間複雜度,不再需要額外的空間。基本思路是遍歷原數組,如果遇到了奇數元素,就將該元素向前移動,該元素前面的偶數元素都依次向後移動。
舉個例子:比如數組{1, 2, 4, 3, 5}
遍曆數組,得到第一個元素是奇數1,其前面沒有元素所以不做移動
第二個,第三個是偶數,不做處理。
第四個元素是奇數3,所以將3往前移動,3前面的偶數元素{2, 4}都向後移動。移動后的數組為{1, 3, 2, 4, 5}
接着第五個元素是奇數5,所以將5往前移動,5前面的偶數元素{2, 4}都向後移動。移動后的數組為{1, 3, 5, 2, 4}
可以這樣理解,每發現一個奇數時,就將這個奇數移動到了它最終應該在的位置上。

實現代碼

public int[] reOrderArray(int[] array)
{
    for(int i = 0; i < array.Length; i++)
    {
        if((array[i] % 2) != 0)
        {
            int temp = array[i];
            int j = i - 1;
            for(; j >= 0; j--)
            {
                if ((array[j] % 2) != 0)
                    break;
                array[j + 1] = array[j]; 
            }
            array[j + 1] = temp;
        }
    }
    return array;
}

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"