智能應用 - U-SQL 大數據應用程序的擴展性

傳統上,在大數據處理期間,對大數據的數據量、速度和多樣性的處理主要集中在提供能處理海量數據的可縮放平臺、添加近乎實時的處理功能,以及提供處理各種格式(從 CSV 到 JSON,再到自定義二進制格式)輸入數據的相應功能上。經常事後纔想到的一種多樣性是與自定義數據處理相關的多樣性,不僅體現在格式方面,還體現在能否輕鬆地使用自定義算法擴展分析,同時保留查詢語言的聲明性。

一些新式大數據處理和查詢語言正着手解決此問題。特別是 U-SQL,一種從頭到腳專爲以下用途而設計的語言:將 SQL 語言的聲明性與靈活使用現有代碼庫和開發新的自定義算法相結合。

使用 U-SQL,你不僅可以添加自己的自定義 C# 函數,還可以通過一個框架添加自己的用戶定義運算符 (UDO),如你自己的提取程序、輸出程序和行集運算符(如處理程序、應用方、化簡程序和自定義合併程序)。此框架由以下兩部分組成:

.NET 接口,爲你提供生成這些運算符的協定,以便你可以專注於代碼編寫,將橫向擴展留給 U-SQL 來執行。請注意,不必在 .NET 中實現實際的業務邏輯代碼,我將在後面予以介紹。
U-SQL 表達式(如 EXTRACT 和 REDUCE),調用自定義運算符,並對數據大規模執行這些運算符。
在本文中,我將接着上一篇文章介紹如何利用 U-SQL 擴展性機制處理各種不同的數據(從 JSON 到圖像數據)。此外,我還將介紹如何添加你自己的運算符。

在 U-SQL 中管理自定義代碼

在開始引入一些示例之前,我們最好先了解一下 U-SQL 是如何使用自定義代碼的。

如上所述,U-SQL 沿用 C# 及其標量表達式語言,適用於 U-SQL 謂詞和 select 子句表達式等場景。爲了讓你的自定義代碼對 U-SQL 編譯器可見,必須將代碼打包到必須由 U-SQL 腳本引用的 .NET 程序集中。必須先使用 CREATE ASSEMBLY 語句事先在 U-SQL 元數據服務中註冊此程序集,然後才能引用它。

註冊和引用 U-SQL 程序集:我建議使用用於 Visual Studio 的 Azure Data Lake 工具 (aka.ms/adltoolsvs),以便輕鬆生成和註冊與 U-SQL 兼容的程序集。如果你在“類庫(對於 U-SQL 應用程序)”項目(見圖 1)中編寫自定義代碼,可以編寫代碼並生成項目,然後右鍵單擊一下即可直接註冊已生成的程序集 DLL 文件(見圖 2)。

類庫(對於 U-SQL 應用程序)項目

圖 1:類庫(對於 U-SQL 應用程序)項目

註冊 U-SQL 程序集

圖2
然後,只需在 U-SQL 腳本中使用 REFERENCE ASSEMBLY 語句,以便可在 U-SQL 腳本中使用公共類和方法即可,如圖 3 所示。

3:從自定義程序集引用用戶定義函數
REFERENCE ASSEMBLY master.TweetAnalysis;
USING tweet_fns = TweetAnalysis.Udfs;
@t =
  EXTRACT date string,
          time string,
          author string,
          tweet string
  FROM "/Samples/Data/Tweets/Tweets.csv"
  USING Extractors.Csv();
// Get the mentions from the tweet string
@m =
  SELECT origin
       , tweet_fns.get_mentions(tweet) AS mentions
       , author AS mentioned_by
FROM @t;
...

將現有代碼與 U-SQL 程序集結合使用:你經常想要使用現有代碼庫或非 .NET 代碼。若要使用非 .NET 代碼(如本機庫或完全不同的語言運行時,如 Python 或 JavaScript),必須爲非 .NET 代碼包裝 C# 互操作性層(從 U-SQL 調用此層,然後調用非 .NET 代碼),同時封送處理組件之間的數據,並實現 UDO 接口協定。在這種情況下,需要將非 .NET 代碼項目(如本機 .dll 或其他運行時的文件)添加爲其他文件。可在用於註冊程序集的“其他文件”選項中執行此操作。當 .NET 程序集在腳本中獲得引用時,這些文件會自動部署到每個節點,並可用於 .NET 程序集在相應節點的本地工作目錄。

若要使用現有 .NET 庫,你需要在自己的程序集上將現有代碼庫註冊爲託管依賴項。或者,如果你重複使用可直接用於 U-SQL 的庫,請直接在 U-SQL 數據庫中進行註冊。無論屬於上述哪種情況,腳本均必須引用所需的全部 .NET 程序集。

在本文的剩餘部分中,我將在介紹一些最好使用擴展性模型的自定義代碼應用場景中,介紹幾個與這些註冊選項相關的示例。這些應用場景包括:使用自定義化簡程序合併重疊的範圍、處理 JSON 文檔、圖像數據和空間數據。我將挨個介紹。

使用自定義化簡程序合併重疊的範圍

假設你有一個日誌文件,用於跟蹤用戶何時與你的服務交互。此外,還假設用戶能夠以多種方式(例如,在多臺設備或多個瀏覽器窗口中執行必應搜索)與你的服務交互。在準備日誌文件以供日後分析的 U-SQL 作業中,你想要合併重疊的範圍。

例如,如果輸入日誌文件如圖 4 所示,則你需要針對每個用戶合併重疊的範圍(如圖 5 所示)。

圖 4:包含重疊的時間範圍的日誌文件

開始時間 結束時間 用戶名
5:00 AM 6:00 AM ABC
5:00 AM 6:00 AM XYZ
8:00 AM 9:00 AM ABC
8:00 AM 10:00 AM ABC
10:00 AM 2:00 PM ABC
7:00 AM 11:00 AM ABC
9:00 AM 11:00 AM ABC
11:00 AM 11:30 AM ABC
11:40 PM 11:59 PM FOO
11:50 PM 0:40 AM FOO
圖 5:合併重疊的時間範圍後的日誌文件
開始時間 結束時間 用戶名
5:00 AM 6:00 AM ABC
5:00 AM 6:00 AM XYZ
7:00 AM 2:00 PM ABC
11:40 PM 0:40 AM FOO

如果你研究一下此問題,首先會注意到你想要通過定義用戶定義聚合等內容來合併重疊的時間間隔。不過,如果你研究一下輸入數據,則會注意到由於數據未經排序,你要麼需要維持所有可能的時間間隔的狀態,然後將非連續時間間隔合併爲橋接時間間隔,要麼需要針對每個用戶名對時間間隔進行重新排序,以簡化時間間隔的合併。

雖然有序聚合更易於橫向擴展,但 U-SQL 不提供有序的用戶定義聚合程序 (UDAGG)。此外,UDAGG 通常會每組生成一行,而在此示例中,如果爲非連續範圍,我可以每組生成多行。

幸運的是,U-SQL 提供了一種稱爲化簡程序 (bit.ly/2evGsDA) 的可縮放 UDO,可根據使用自定義代碼設置的分組鍵聚合行集。

我們要先編寫 U-SQL 邏輯,其中 ReduceSample.Range­Reducer 是 RangeReducer 程序集中的用戶定義化簡程序(化簡程序 UDO),日誌數據位於文件 /Samples/Blogs/MRys/Ranges/ranges.txt (bit.ly/2eseZyw) 中,並使用“-”作爲列分隔符。代碼如下:

REFERENCE ASSEMBLY RangeReducer;
@in = EXTRACT start DateTime, end DateTime, user string
FROM "/Samples/Blogs/MRys/Ranges/ranges.txt"
USING Extractors.Text(delimiter:'-');
@r =  REDUCE @in PRESORT start ON user
      PRODUCE start DateTime, end DateTime, user string
      READONLY user
      USING new ReduceSample.RangeReducer();
OUTPUT @r
TO "/temp/result.csv"
USING Outputters.Csv();

REDUCE 表達式將行集 @in 視作輸入,根據用戶列對其進行分區,根據起始列中的值對分區進行預排序,然後應用 RangeReducer,從而在輸出中生成相同的行集架構。由於化簡程序僅調整起止範圍,因此它其實並未調整用戶列,因此將用戶列標記爲 READONLY。這樣一來,化簡程序框架便有權自動爲該列傳遞數據,進而允許 U-SQL 查詢處理程序出於一些目的主動優化只讀列,如爲了先於化簡程序在只讀列上疊加謂詞。

編寫化簡程序的方法是實現 Microsoft.Analytics.Interfaces.IReducer 實例。在此示例中,由於不必提供任何參數,因此只需覆蓋抽象的 Reduce 方法。可以將代碼複製到適用於 U-SQL 的 C# 庫中,然後將它註冊爲程序集 RangeReducer,如上所述。圖 6 展示了 RangeReducer 的實現。(請注意,由於空間有限,已改變一些代碼示例中的常規代碼縮進做法。)

6:RangeReducer 的 C# 實現
using Microsoft.Analytics.Interfaces;
using Microsoft.Analytics.Types.Sql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ReduceSample
{
  public class RangeReducer : IReducer
  {
    public override IEnumerable<IRow> Reduce(
      IRowset input, IUpdatableRow output)
    {
      // Init aggregation values
      bool first_row_processed = false;
      var begin = DateTime.MaxValue;
      var end = DateTime.MinValue;
      // Requires that the reducer is PRESORTED on begin and
      // READONLY on the reduce key.
      foreach (var row in input.Rows)
      {
        // Initialize the first interval with the first row if i is 0
       if (!first_row_processed)
        {
         first_row_processed = true; // Mark that the first row was handled
          begin = row.Get<DateTime>("start");
          end = row.Get<DateTime>("end");
          // If the end is just a time and not a date, it can be earlier
          // than the begin, indicating it is on the next day;
          // this let's you fix up the end to the next day in that case
          if (end < begin) { end = end.AddDays(1); }
        }
        else // Handle the remaining rows
        {
          var b = row.Get<DateTime>("start");
          var e = row.Get<DateTime>("end");
          // Fix up the date if end is earlier than begin
          if (e < b) { e = e.AddDays(1); }
          // If begin time is still inside the interval,
          // increase the interval if it is longer
          if (b <= end)
          {
            // If the new end time is later than the current,
            // extend the interval
            if (e > end) { end = e; }
          }
          else // Output the previous interval and start a new one
          {
            output.Set<DateTime>("start", begin);
            output.Set<DateTime>("end", end);
            yield return output.AsReadOnly();
            begin = b; end = e;
          } // if
        } // if
      } // foreach
      // Now output the last interval
      output.Set<DateTime>("start", begin);
      output.Set<DateTime>("end", end);
      yield return output.AsReadOnly();
    } // Reduce
  } // RangeReducer
} // ReduceSample

U-SQL REDUCE 表達式會對各個不同的分區鍵並行應用一次 Reduce 方法。因此,輸入參數僅包含給定組的行,且實現可返回零到多行作爲輸出。

由於 PRESORT 子句保證行已經過排序,因此內部邏輯可假設數據已經過排序。又因爲用戶列被標記爲 READONLY,所以將自動傳遞列數據,而你只需關注要轉換的列,便可以更慣常的做法編寫 UDO 代碼。

如果你現在對大型數據集應用化簡程序,且一些用戶使用你係統的頻次可能高於另一些用戶,那麼你會遇到所謂的數據傾斜問題,即一些用戶具有大型分區,而另一些用戶僅具有小型分區。由於可保證化簡程序協定查看相應分區的所有數據,因此必須將所有數據都無序播放到相應節點,然後在一次調用中予以讀取。鑑於這一要求,最好的情況是,此類數據傾斜可能會導致一些分區的處理時間長於另一些分區;最壞的情況是,此類數據傾斜可能會導致某些化簡程序用盡可用的內存和時間資源(運行約五小時後,U-SQL 頂點將超時)。

如果化簡程序語義具有關聯性和可交換性,且其輸出架構與輸入架構相同,則可將化簡程序標記爲遞歸,這可允許查詢引擎將大型組拆分爲較小的子組,然後在這些子組上以遞歸方式應用化簡程序,從而計算最終結果。此遞歸應用程序允許化簡程序在出現數據偏斜時更好地平衡和並行化。化簡程序是使用屬性註釋 SqlUserDefinedReducer(IsRecursive = true) 標記爲遞歸:

namespace ReduceSample
{
  [SqlUserDefinedReducer(IsRecursive = true)]
  public class RangeReducer : IReducer
  {
    public override IEnumerable<IRow> Reduce(
      IRowset input, IUpdatableRow output)
    {
      // Insert the code from Figure 6 here
    } // Reduce
  } // RangeReducer
} // ReduceSample

在我們的示例中,可將化簡程序標記爲遞歸,從而提升可縮放性和性能(假設每次遞歸調用時處理功能將保留行之間的排序)。

有關 Visual Studio 項目示例,請訪問我們的 GitHub 存儲庫 (bit.ly/2ecLe5B)。

處理 JSON 文檔

除了逗號分隔文本文件之外,最常見的一種數據格式是 JSON。不同於 CSV 文件格式,U-SQL 不提供內置的 JSON 提取程序。不過,U-SQL 社區提供了一個示例程序集 (bit.ly/2d9O4va),可支持提取和處理 JSON 和 XML 文檔。

這一解決方案使用 Newtonsoft 的 Json.NET 庫 (bit.ly/2evWJbz) 執行繁重的 JSON 處理工作,並使用 System.XML 處理 XML。此程序集可使用 JsonExtractor (bit.ly/2dPARsM) 從 JSON 文檔提取數據,並能通過 JsonTuple 函數 (bit.ly/2e8tSuX) 獲取 JSON 文檔並將其拆分爲 SqlMap(以便導航和分解 JSON 文檔),最後可通過 JSONOutputter (bit.ly/2e4uv3W) 將行集轉換成 JSON 格式文件。

請注意,根據設計,此程序集爲通用 JSON 處理程序。也就是說,它不會假設 JSON 文檔結構,需要適應 JSON 的半結構化特性,包括異類鍵入的元素(標量與結構化、同一元素的不同數據類型、缺少的元素等)。如果你瞭解到 JSON 文檔遵循某種特定的架構,可以創建更加有效的 JSON 提取程序。

不同於前面介紹的化簡程序示例(你需要編寫自己的程序集以供稍後部署),在此示例中,這一解決方案隨時可用。你可以將這一解決方案從我們的 GitHub 存儲庫加載到 Visual Studio 中,然後自行生成和部署,也可以在這一解決方案的 bin\Debug 目錄中找到 DLL。

如前所述,非系統依賴項要求必須在 U-SQL 元數據存儲中同時註冊 Samples.Format 和 Json.NET 程序集(你可以在使用 Visual Studio 工具註冊 Format 程序集時,選擇 Newtonsoft 程序集作爲託管依賴項),且必須同時引用這兩個程序集(如果你要處理 JSON 文檔的話)。假設你已在 U-SQL 目錄的 U-SQL 數據庫 JSONBlog 中以 [Microsoft.Analytics.Samples.Formats] 和 [NewtonSoft.Json] 名稱安裝 JSON 程序集(見圖 7),可以使用它們,方法爲在腳本開頭通過以下代碼引用它們:

REFERENCE ASSEMBLY JSONBlog.[NewtonSoft.Json];
REFERENCE ASSEMBLY JSONBlog.[Microsoft.Analytics.Samples.Formats];
在 Visual Studio 中註冊 Formats 程序集
圖 7:在 Visual Studio 中註冊 Formats 程序集

JSON 提取程序實現 U-SQL IExtractor 接口。由於需要完全分析 JSON 文檔以確保其格式正確,因此需要在單個提取程序頂點處理包含單個 JSON 文檔的文件。所以,可通過將 AtomicFileProcessing 屬性設置爲 true,指明提取程序需要查看完整文件內容(見圖 8)。可使用可選參數 rowpath 調用提取程序。藉助此參數,我們可以標識將使用 JSONPath 表達式 (bit.ly/1EmvgKO) 映射到行的各個 JSON 對象。

8:JSON 提取程序
[SqlUserDefinedExtractor(AtomicFileProcessing = true)]
public class JsonExtractor : IExtractor
{
  private string rowpath;            
  public JsonExtractor(string rowpath = null)
  {
    this.rowpath = rowpath;
  }
  public override IEnumerable<IRow> Extract(
    IUnstructuredReader input, IUpdatableRow output)
  {
    // Json.NET
    using (var reader = new JsonTextReader(
      new StreamReader(input.BaseStream)))
    {
      // Parse Json
      var root = JToken.ReadFrom(reader);
      // Rows
      // All objects are represented as rows
      foreach (JObject o in SelectChildren(root, this.rowpath))
      {
        // All fields are represented as columns
        this.JObjectToRow(o, output);
        yield return output.AsReadOnly();
      }
    }
  }
}

通過提取程序實現,可將 U-SQL 提取程序框架饋送給提取程序的輸入流傳遞到 Json.NET JsonTextReader。然後,它使用 rowpath 獲取通過 SelectChildren 映射到行的子樹。因爲 JSON 對象可以是異類的,所以代碼返回的是常規 JObject,而不是位置 JArray 或標量值。

請注意,此提取程序要將 JSON 文檔加載到內存中。如果文檔太大,可能會導致內存不足。在這種情況下,你不得不編寫自己的提取程序,以流式傳輸文檔,而無需將整個文檔加載到內存中。

現在,讓我們來使用 JSON 提取程序和 JSON 元組函數分析 /Samples/Blogs/MRys/JSON/complex.json 中的複雜 JSON 文檔 (bit.ly/2ekwOEQ),如圖 9 所示。

9JSON 示例文檔
[{
  "person": {
    "personid": 123456,
    "name": "Person 1",
    "addresses": {
      "address": [{
        "addressid": "2",
        "street": "Street 2",
        "postcode": "1234 AB",
        "city": "City 1"
      }, {
        "addressid": "2",
        "street": "Street 2",
        "postcode": "5678 CD",
        "city": "City 2"
      }]
    }
  }
}, {
     "person": {
     "personid": 798,
     "name": "Person 2",
     "addresses": {
       "address": [{
         "addressid": "1",
         "street": "Street 1",
         "postcode": "1234 AB",
         "city": "City 1"
     }, {
         "addressid": "4",
         "street": "Street 7",
         "postcode": "98799",
         "city": "City 3"
     }]
   }
  }
}]

格式爲一組人員“對象”(從技術角度來講,每個對象包含一個人員鍵),這樣就可以包含一些人員屬性和地址對象。圖 10 中的 U-SQL 腳本每個人員/地址組合提取一行。

10:處理圖 9 中的示例 JSON 文檔的 U-SQL 腳本
DECLARE @input string = "/Samples/Blogs/MRys/JSON/complex.json";
REFERENCE ASSEMBLY JSONBlog.[Newtonsoft.Json];
REFERENCE ASSEMBLY JSONBlog.[Microsoft.Analytics.Samples.Formats];
USING Microsoft.Analytics.Samples.Formats.Json;
@json =
  EXTRACT personid int,
          name string,
          addresses string
  FROM @input
  USING new JsonExtractor("[*].person");
@person =
  SELECT personid,
         name,
         JsonFunctions.JsonTuple(
           addresses, "address")["address"] AS address_array
  FROM @json;
@addresses =
  SELECT personid,
         name,
         JsonFunctions.JsonTuple(address) AS address
  FROM @person
       CROSS APPLY
         EXPLODE (JsonFunctions.JsonTuple(address_array).Values)
           AS A(address);
@result =
  SELECT personid,
         name,
         address["addressid"]AS addressid,
         address["street"]AS street,
         address["postcode"]AS postcode,
         address["city"]AS city
  FROM @addresses;
OUTPUT @result
TO "/output/json/persons.csv"
USING Outputters.Csv();

請注意,此腳本將 JSONPath 表達式 [*].person 傳遞給提取程序,從而爲頂級數組中的每個人員字段生成一行。提取程序使用 EXTRACT 架構將生成的對象屬性提取到列中。由於地址字段本身就是嵌套的 JSON 文檔,因此首次調用 JsonTuple 函數會創建包含地址對象的映射,隨後每個地址對象會通過 CROSS APPLY EXPLODE 表達式映射到一行。最後,從映射數據類型中提取出所有地址屬性,從而生成行集,如圖 11 所示。

圖 11:處理圖 9 中的 JSON 文檔後生成的行集

123456 人員 1 2 街道 2 1234 AB 城市 1
123456 人員 1 2 街道 2 5678 CD 城市 2
798 人員 2 1 街道 1 1234 AB 城市 1
798 人員 2 4 街道 7 98799 城市 3

有關 Visual Studio 項目示例和其他 JSON 處理應用場景(包括一個文件內含多個 JSON 文檔),請訪問我們的 GitHub 存儲庫 (bit.ly/2dzceLv)。

處理圖像數據

在此示例中,我將處理一些較大的非結構化數據,即圖像數據。特別是,我想處理 JPEG 圖片,並提取一些 JPEG EXIF 屬性。此外,我還想創建圖像的縮略圖。幸運的是,.NET 在 System.Drawing 類中提供了各種圖像處理功能。因此,只需生成 U-SQL 擴展函數和運算符,即可將 JPEG 處理委派給這些類。

有多種方法可以做到這一點。初步嘗試可能是將所有圖像作爲字節數組加載到行集中,然後應用各個用戶定義函數來提取每個屬性並創建縮略圖,如圖 12 所示。

12:將圖像加載到行中,從而在 U-SQL 中處理圖像
REFERENCE ASSEMBLY Images;
USING Images;
@image_data =
  EXTRACT image_data byte[]  // Max size of row is 4MB!
        , name string
        , format string
  FROM @"/Samples/Data/Images/{name}.{format}"
  USING new ImageExtractor();
// Use UDFs
@image_properties =
  SELECT ImageOps.getImageProperty(image_data, ImageProperties.copyright)
         AS image_copyright,
         ImageOps.getImageProperty(image_data, ImageProperties.equipment_make)
         AS image_equipment_make,
         ImageOps.getImageProperty(image_data, ImageProperties.equipment_model)
         AS image_equipment_model,
         ImageOps.getImageProperty(image_data, ImageProperties.description)
         AS image_description
  FROM @image_data
  WHERE format IN ("JPEG", "jpeg", "jpg", "JPG");

不過,這種方法存在一些缺點:

由於 U-SQL 行大小上限爲 4MB,因此解決方案的圖像大小(減去其他列的大小)不得超過 4MB。
每個函數調用都有可能會增加內存壓力,需要在整個 U-SQL 處理過程中傳遞字節數組。
因此,最好直接在自定義提取程序中提取屬性和創建縮略圖。圖 13 展示了修訂後的 U-SQL 腳本。

13:使用提取程序提取特性,從而在 U-SQL 中處理圖像
REFERENCE ASSEMBLY Images;
@image_features =
  EXTRACT copyright string,
          equipment_make string,
          equipment_model string,
          description string,
          thumbnail byte[],
          name string,
          format string
  FROM @"/Samples/Data/Images/{name}.{format}"
  USING new Images.ImageFeatureExtractor(scaleWidth:500, scaleHeight:300);
@image_features =
  SELECT *
  FROM @image_features
  WHERE format IN ("JPEG", "jpeg", "jpg", "JPG");
OUTPUT @image_features
TO @"/output/images/image_features.csv"
USING Outputters.Csv();
@scaled_image =
  SELECT thumbnail
  FROM @image_features
  WHERE name == "GT4";
OUTPUT @scaled_image
TO "/output/images/GT4_thumbnail_2.jpg"
USING new Images.ImageOutputter();

此腳本從文件集模式 /Samples/Data/Images/{name}.{format} (bit.ly/2ektTY6) 指定的圖像中提取屬性和縮略圖。然後,SELECT 語句限制爲僅提取 JPEG 文件,具體方法爲僅在格式列上使用謂詞,將所有非 JPEG 文件排除在提取範圍外(優化程序僅將提取程序應用於滿足格式列上謂詞的文件)。提取程序允許指定縮略圖的尺寸。然後,腳本將特性輸出到 CSV 文件中,並使用簡單的“字節-流-級別”輸出程序爲其中一個縮小圖像創建縮略圖文件。

圖 14 展示了提取程序的實現。

14:圖像特性提取程序
using Microsoft.Analytics.Interfaces;
using Microsoft.Analytics.Types.Sql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
namespace Images
{
  public static class UpdatableRowExtensions
  {
    public static void SetColumnIfExists<T>(this IUpdatableRow source
                                           , string colName, T value)
    {
      var colIdx = source.Schema.IndexOf(colName);
      if (colIdx != -1)
      { source.Set<T>(colIdx, value); }
    }
  }
  [SqlUserDefinedExtractor(AtomicFileProcessing = true)]
  public class ImageFeatureExtractor : IExtractor
  {
    private int _scaleWidth, _scaleHeight;
    public ImageFeatureExtractor(int scaleWidth = 150, int scaleHeight = 150)
    { _scaleWidth = scaleWidth; _scaleHeight = scaleHeight; }
    public override IEnumerable<IRow> Extract(IUnstructuredReader input
                                             , IUpdatableRow output)
    {
      byte[] img = ImageOps.GetByteArrayforImage(input.BaseStream);
      using (StreamImage inImage = new StreamImage(img))
      {
        output.SetColumnIfExists("image", img);
        output.SetColumnIfExists("equipment_make",
          inImage.getStreamImageProperty(ImageProperties.equipment_make));
        output.SetColumnIfExists("equipment_model",
          inImage.getStreamImageProperty(ImageProperties.equipment_model));
        output.SetColumnIfExists("description",
          inImage.getStreamImageProperty(ImageProperties.description));
        output.SetColumnIfExists("copyright",
          inImage.getStreamImageProperty(ImageProperties.copyright));
        output.SetColumnIfExists("thumbnail",
          inImage.scaleStreamImageTo(this._scaleWidth, this._scaleHeight));
      }
      yield return output.AsReadOnly();
    }
  }
}

此提取程序也需要查看整個文件,並在 input.BaseStream 上運行,但現在僅在內存中創建一個圖像,不同於圖 12 中的腳本。此提取程序還檢查是否有各個請求的列,並使用擴展方法 SetColumnIfExists 僅處理請求的列名稱的數據。

如需瞭解更多詳情,請參閱 GitHub 站點上的 Visual Studio 項目 (bit.ly/2dngXCE)。

處理空間數據

在此示例中,我將展示如何在 U-SQL 中使用 SQL Server 空間類型程序集 Microsoft.SqlServer.Types.dll。特別是,我想在 U-SQL 腳本中將空間庫函數用作用戶定義函數。如前面介紹的 JSON 提取程序,這意味着你需要在 JSON 中註冊現有程序集,而無需編寫自己的程序集。

首先,你需要從 SQL Server 2016 功能包 (bit.ly/2dZTw1k) 下載並安裝程序集。選擇 64 位版本安裝程序 (ENU\x64\SQLSysClrTypes.msi),以確保擁有 64 位版本的庫。

此安裝程序將託管程序集 Microsoft.Sql­­Server.Types.dll 安裝到 C:\Program Files (x86)\Microsoft SQL Server\130\SDK\Assemblies 中,並將本機程序集 SqlServerSpatial130.dll 安裝到 \Windows\System32\ 中。接下來,將程序集上載到 Azure Data Lake Store(例如,文件夾 /upload/asm/spatial)中。由於安裝程序已將本機庫安裝到系統文件夾 c:\Windows\System32 中,因此,請務必在上載之前從此文件夾複製 SqlServerSpatial130.dll,或確保你所用的工具不會在系統文件夾上執行文件系統重定向 (bit.ly/1TYm9YZ)。例如,如果你想要使用最新的 Visual Studio ADL 文件資源管理器上載它,必須先將此文件複製到其他目錄中;否則,(自本文編寫之時起)你將上載的是 32 位版本(因爲 Visual Studio 是 32 位應用程序,在 ADL 上載文件選擇窗口中執行文件系統重定向),而且你會在運行調用本機程序集的 U-SQL 腳本時看到以下運行時(內部)錯誤: “來自用戶表達式的內部異常: 嘗試加載格式錯誤的程序。(HRESULT 發生異常: 0x8007000B)”。

上載兩個程序集文件後,在 SQLSpatial 數據庫中向此腳本註冊它們:

DECLARE @ASSEMBLY_PATH string = “/upload/asm/spatial/”;
DECLARE @SPATIAL_ASM string = @ASSEMBLY_PATH+”Microsoft.SqlServer.Types.dll”;
DECLARE @SPATIAL_NATIVEDLL string = @ASSEMBLY_PATH+”SqlServerSpatial130.dll”;
CREATE DATABASE IF NOT EXISTS SQLSpatial;
USE DATABASE SQLSpatial;
DROP ASSEMBLY IF EXISTS SqlSpatial;
CREATE ASSEMBLY SqlSpatial
FROM @SPATIAL_ASM
WITH ADDITIONAL_FILES =
(
@SPATIAL_NATIVEDLL
);
請注意,在此示例中,僅註冊一個 U-SQL 程序集,然後將本機程序集作爲強依賴項添加到 U-SQL 程序集中。若要使用空間程序集,你只需引用 U-SQL 程序集,其他文件會自動對此程序集可用。圖 15 展示了使用空間程序集的簡單腳本示例。

15:在 U-SQL 中使用空間功能
REFERENCE SYSTEM ASSEMBLY [System.Xml];
REFERENCE ASSEMBLY SQLSpatial.SqlSpatial;
USING Geometry = Microsoft.SqlServer.Types.SqlGeometry;
USING Geography = Microsoft.SqlServer.Types.SqlGeography;
USING SqlChars = System.Data.SqlTypes.SqlChars;
@spatial =
    SELECT * FROM (VALUES
                   // The following expression is not using the native DDL
                   ( Geometry.Point(1.0,1.0,0).ToString()),   
                   // The following expression is using the native DDL
                   ( Geometry.STGeomFromText(
                     new SqlChars("LINESTRING (100 100, 20 180, 180 180)"),
                     0).ToString())
                  ) AS T(geom);
OUTPUT @spatial
TO "/output/spatial.csv"
USING Outputters.Csv();

由於 SQL 類型庫依賴 System.Xml 程序集,因此需要引用它。此外,一些方法使用的是 System.Data.SqlTypes 類型,而不是內置的 C# 類型。由於 System.Data 已默認包括在內,因此你只需引用所需的 SQL 類型即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章