顯示具有 C# 程式設計筆記 標籤的文章。 顯示所有文章
顯示具有 C# 程式設計筆記 標籤的文章。 顯示所有文章

2015年5月7日 星期四

Async/Await、Task產生執行緒

之前一直認為,Async/Await跟Task一樣都會建出新的執行緒,這是錯誤的,Await是用主執行緒,遇到真正的Async方法(ex:HttpClient.GetAsync)或手動執行的Task.Run或Task.Factory.StartNew,這樣才會建立出新的執行緒

//主執行緒ID
Console.WriteLine(string.Format("Main:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId));

//Task執行緒ID
Task t = Task.Run(() => { Console.WriteLine(string.Format("Task:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); });

//使用Await呼叫方法
await GetAwaitThreadId();

//等候Task完成
t.Wait();

async Task GetAwaitThreadId()
{
  //Await方法執行緒ID
  Console.WriteLine(string.Format("Await:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
}



另外,當呼叫非同步方法時,在前半部的程式碼(Await之前),依然是原本的執行緒在執行,遇到Await時,原本的執行緒會回到呼叫端
後半部的程式碼(Await之後),會暫時被保留,等到要等待的工作完成以後,會另外找一條執行緒出來執行後半部程式碼
//呼叫方法
Task t = PrintAwaitThreadId();
for (int i = 0; i < 10; i++)
{
  //列出目前執行緒
  Console.WriteLine(string.Format("Main[{1}]:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId, i));
  //產生新的執行緒做時間延遲
  await Task.Delay(rnd.Next(1, 10));
}
await t;

async Task PrintAwaitThreadId()
{
  for (int i = 0; i < 10; i++)
  {
    //列出目前執行緒
    Console.WriteLine(string.Format("Await[{1}]:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId, i));
    //產生新的執行緒做時間延遲
    await Task.Delay(rnd.Next(1, 10));
  }
}





2015年4月14日 星期二

使用 Windows Azure Redis 快取

有些資料需要大量運算,但是變動機會極低的時候,常會把運算結果存起來,下次直接抓出結果即可。

如果有使用Windows Azure,這邊提供一個快取方式。

參考:如何使用 Azure Redis 快取

首先建立Redis Cache Service

安裝StackExchange.Redis並加入參考

先來看一下Code:
  /// <summary>
  /// Cache Redis 連線字串,來自Web.config > AppSettings > CacheRedisConnectionString
  /// </summary>
  private static string _CacheRedisConnectionString = "{名稱}.redis.cache.windows.net,ssl=true,password={密碼}";
  /// <summary>
  /// Cache Redis 預設保留時間
  /// </summary>
  private static int _CacheRedisTimeOut = 60;
  /// <summary>
  /// Cache Redis 連線(類似SQLConnection)
  /// </summary>
  private static ConnectionMultiplexer CacheRedisConnection;
  /// <summary>
  /// Cache Redis 存取(類似entity framework的DbContext)
  /// </summary>
  public static IDatabase CacheRedis;

  /// <summary>
  /// 設定快取初始連線
  /// </summary>
  public static void Initialize()
  {
      if(CacheRedisConnection == null)
      {
    CacheRedisConnection = ConnectionMultiplexer.Connect(_CacheRedisConnectionString);
    if(CacheRedis == null)
    {
        CacheRedis = CacheRedisConnection.GetDatabase();
    }
      }
  }

  /// <summary>
  /// 序列化物件
  /// </summary>
  /// <param name="value"></param>
  /// <returns></returns>
  private static string Serialize(object value)
  {
      if (value == null)
    return null;
      return JsonConvert.SerializeObject(value);
  }

  /// <summary>
  /// 字串反序列化為物件
  /// </summary>
  /// <typeparam name="T">回傳物件型別</typeparam>
  /// <param name="value">字串資料</param>
  /// <returns>物件</returns>
  private static T Deserialize<T>(string value)
  {
      if (string.IsNullOrWhiteSpace(value)) return default(T);
      return JsonConvert.DeserializeObject<T>(value);
  }

  /// <summary>
  /// 取得指定型別快取物件
  /// </summary>
  /// <typeparam name="T">回傳物件型別</typeparam>
  /// <param name="key">快取名稱</param>
  /// <returns>回傳快取資料</returns>
  public static T GetCache<T>(string key)
  {
      string value = GetCache(string key);
      if(string.IsNullOrWhiteSpace(value) == true) return null;
      return Deserialize<T>(GetCache(string key));
  }

  /// <summary>
  /// 取得快取物件
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <returns>回傳快取資料</returns>
  public static string GetCache(string key)
  {
      //cache為null 或 key為空 或 Cache無資料,回傳null
      if (CacheRedis == null || string.IsNullOrWhiteSpace(key) == true || CacheRedis.KeyExists(key) == false) return null;
      return CacheRedis.StringGet(key);
  }

  /// <summary>
  /// 設定快取物件,(選用)設定系統預設快取保留時間
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <param name="value">要快取的資料</param>
  /// <param name="defaultexpiry">是否設定預設快取保留時間</param>
  public static void SetCache(string key, object value, bool defaultexpiry = true)
  {
      SetCache(key, value, 0, defaultexpiry);
  }

  /// <summary>
  /// 設定快取物件,並設定快取保留時間
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <param name="value">要快取的資料</param>
  /// <param name="expiry">快取保留時間(分鐘)</param>
  public static void SetCache(string key, object value, int expiry)
  {
      if (expiry <= 0) throw new Exception("CacheRedisHelpers.SetCache(string key, object value, int expiry) error : No Set Expiry");
      SetCache(key, value, expiry, false);
  }

  /// <summary>
  /// (private)設定快取物件
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <param name="value">要快取的資料</param>
  /// <param name="expiry">快取保留時間(分鐘)</param>
  /// <param name="defaultexpiry">是否設定預設快取保留時間</param>
  private static void SetCache(string key, object value, int expiry, bool defaultexpiry)
  {
      //CacheRedis為null 或 key為空,則不處理
      if (CacheRedis != null && string.IsNullOrWhiteSpace(key) == false)
      {
    //value不為null,則設定快取,反之刪除
    if (value != null)
    {
        //物件轉字串
        string strvalue = value as string ?? Serialize(value);
        
        //存入Cache
        if (defaultexpiry) //使用預設快取保留時間
      CacheRedis.StringSet(key, strvalue , TimeSpan.FromMinutes(_CacheRedisTimeOut));
        else if (expiry > 0) //使用傳入快取保留時間
      CacheRedis.StringSet(key, strvalue , TimeSpan.FromMinutes(expiry));
        else //不設定快取保留時間
      CacheRedis.StringSet(key, strvalue );
    }
    else CacheRedis.KeyDelete(key);
      }
  }

在程式中,寫好建立連線的Method(Initialize),
建立共用序列化與反序列化Method,
取得的部分,如果取不到資料,Redis Cache也是回傳null,Method只是添加一些判斷減少跟Service連線,
儲存的部分,建立2種較彈性的設定保留時間Method,一個是使用預先設定的預設快取保留時間,另一個是自行設定快取保留時間,
完成了Redis Cache存取功能,接下來使用方式如下:
  //建立連線
  Initialize();
  
  //儲存資料(預設保留時間)
  SetCache("key", objvalue, true);

  //儲存資料(自訂保留時間)
  SetCache("key", objvalue, iexpiry);

  //取得資料(String)
  GetCache("key");

  //取得資料(指定Type)
  GetCache("key");

另外,在CacheRedisConnection.GetDatabase(),可以指定不同的DB(int)儲存資料(預設為0),可以將資料區分讓開發者依不同情境區分資料存放位置。

2014年7月11日 星期五

時區到各瀏覽器顯示時間統一處理方式

今天要討論的是,當日期時間資料送到瀏覽器時,顯示出來的結果往往讓人難以捉摸。

DateTimeDateTimeOffset轉成JSON以後的結果:
{
  "DateTimeOffset": "2014-07-11T00:00:00+08:00",
  "DateTime": "2014-07-11T00:00:00"
}
DateTimeOffset後面多了+08:00,表示這個時間是特定時區的時間,而DateTime是沒有的。

這時候丟到JavaScript的Date裡面:
  new Date(DateTimeOffset);
  new Date(DateTime);
呈現在畫面上就會變成:
Browser DateTimeOffset DateTime
Chrome Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
IE11 Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間)
FireFox Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間)
當沒有指定時區時,顯示出來的結果因各瀏覽器定義不同,而有不同的結果。

當不想要資料庫所有欄位都記時區的時候,就需要變更一下Json的DateTimeConverter

首先宣告一個UTCDateTimeConverter,並且繼承IsoDateTimeConverter ,然後在轉JSON的時候自動轉換成設定的格式

備註1:希望給瀏覽器自動轉換為Local時間,所以是轉換成UTC(國際標準時間)
備註2:避免日後Newtonsoft.Json對IsoDateTimeConverter更新,所以不重寫WriteJson,使用設定轉換格式並由IsoDateTimeConverter的WriteJson進行轉換。
/// <summary>
/// DateTime/DateTimeOffset 傳換為UTC日期格式 (Ex: 2014-07-11T12:53:00+00:00).
/// </summary>
public class UTCDateTimeConverter : IsoDateTimeConverter
{
  /// <summary>
  /// 日期轉成Json格式
  /// </summary>
  /// <param name="writer" >The JsonWriter to write to.</param>
  /// <param name="value" >The value.</param>
  /// <param name="serializer" >The calling serializer.</param>
  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    //DateTimeOffset轉換時會自動加上時區,所以要先把他轉成UTC時間的DateTime
    if (value.GetType() == typeof(DateTimeOffset))
    {
      DateTimeOffset tmpDateTimeOffset = (DateTimeOffset)value;
      value = tmpDateTimeOffset.DateTime.Add(-tmpDateTimeOffset.Offset);
    }
    //設定想要轉換的格式
    base.DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK+00:00";
    //使用預設的WriteJson進行轉換
    base.WriteJson(writer: writer, value: value, serializer: serializer);
  }
}
因為希望傳出去時間格式是統一的,而使用的也是WebApi,所以直接在App_Start/WebApiConfig.cs進行設定:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new UTCDateTimeConverter());

轉出來的Json為:
{
  "DateTimeOffset": "2014-07-10T16:00:00+00:00",
  "DateTime": "2014-07-11T00:00:00+00:00"
}
DateTimeOffset已經是被剪了8小時。

呈現在畫面上就會變成:
Browser DateTimeOffset DateTime
Chrome Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
IE11 Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
FireFox Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)


瀏覽器都統一了^^

參考:
IsoDateTimeConverter Source
Add different Json.NET DateTimeConverters through the JsonFormatter's SerializerSettings
Create a JSON.NET Date to String custom Converter

2014年7月8日 星期二

ASP .NET MVC4 WebApi-CultureInfo-文化特性排序

相信很多人都發生過,本機排序正常,一上到正式主機以後,排序出來的結果卻不一樣。

其實.NET的執行緒就跟SQL Server的資料表一樣有定序問題,所以排序結果會因為設定而有所不同,基本上執行緒的定序是跟著Server預設語系的。

在變更執行緒的語系之前,先看一下CultureInfo類別

MSDN的說明是:提供有關特定文化特性 (Culture) 的資訊 (文化特性在 Unmanaged 程式碼開發中稱為「地區設定」(Locale))。 提供的資訊包括文化特性的名稱、書寫系統、使用的曆法,以及日期和排序字串的格式。

最後面那一句話,排序字串的格式

了解了這個類別的用途,接著來看一下幾個語系排序的規則。

文化特性名稱
文化特性
預設的排序名稱和識別項
替代排序名稱和識別項
zh-TW
中文 (台灣)
筆劃:0x00000404
注音符號:0x00030404
zh-CN
中文 (中華人民共和國)
發音:0x00000804
筆劃:0x00020804
ja-JP
日文 (日本)
預設:0x00000411
Unicode:0x00010411

知道各語系排序規則以後,接著我們只需要在排序前加上一個指定就搞定了。

System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(language);

這樣是不是很簡單呢。(明明為了排序搞了很久)

參考:
CultureInfo與中文字串排序
CultureInfo 類別


2014年5月15日 星期四

Entity Framework 與 LINQ -- 篩選(Where)使用時機

最近發現,使用Entity Framework的話,要注意下Where的時機,因為有可能是會在SQL端篩選,也有可能會在C#端篩選,可能會導致不同的結果,甚至發生不支援的情況。

拿字串A是否包含了字串B作為範例,這時候在SQL端(還沒產生成實體物件)時執行結果 跟 C#端(已從DB撈出資料)執行結果 就會發生不同的狀況。
先來看一Where下在資料還沒產生成實體物件的時候
C#:
var query = db.Table.Where(x => x.Name.Contains("abc")).ToList();
SQL執行語法會是
SELECT[EmployeeId],[Name]
FROM [dbo].[Employee]
WHERE [Name] LIKE '%abc%'
由此可見,資料判斷是在SQL端處理,然後把結果丟回到C#端。

再來是Where下在ToList()後面
C#:
var query = db.Table.ToList().Where(x => x.Name.Contains("abc"));
SQL執行語法會是
SELECT[EmployeeId],[Name]
FROM [dbo].[Employee]
這時候變成了把所有Employee資料撈出來以後,在篩選資料。

一個是在SQL端下LIKE,一個是在C#端執行Contains,都是包含"abc",但是結果就會不一樣囉。

SQL的LIKE是不分辨大小寫的,所以"ABC"、"AbC"、"abc"、"abC"...都是會被認為是OK的資料。
但是在C#的Contains是有區分大小寫的,所以只有"abc"才會被篩選出來。

如果使用IndexOf去指定不區分大小寫,但是這個只能在C#端使用
錯誤的C#語法:
var query = db.Table.Where(x => x.Name.IndexOf("abc", StringComparison.CurrentCultureIgnoreCase) >= 0).ToList();
這樣會報錯,因為LINQ在轉SQL語法的時候會發生不支援的情況,就回歸到一開始提到的Where時機。
所以我們要在取出資料後,由C#去執行篩選
C#語法:
var query = db.Table.ToList().Where(x => x.Name.IndexOf("abc", StringComparison.CurrentCultureIgnoreCase) >= 0);

PS:Contains是使用IndexOf去實做出來的,所以基本上IndexOf效能會略好於Contains



2014年5月9日 星期五

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續2) -- 中繼傳遞OData參數

延續前二篇
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料)

範例為建立一個API,把OData參數傳給資料來源Api,然後做簡單處理或直接回傳(資料來源Api開放使用OData)。

建立一個Api
//如果有$inlinecount就會是不同的格式,所以這邊回傳Object
public Object Get()
{
    return "test";
}
取得所有OData參數
List<string> arrParams = Request.GetQueryNameValuePairs() //所有QueryString的Name、Value集合
    .Where(x => x.Key.StartsWith("$")) //只取得開頭為$
    .Select(x => string.Format("&{0}={1}", x.Key, Uri.EscapeDataString(x.Value))) //組回 &Name=Value
    .ToList();
因為範例的資料來源API本身有指定參數,所以這邊會先把 & 加上,方便後面整理所有參數,請視個人情況調整加上的時機。

判斷是否有設定$inlinecount=allpages
有的話就要用C# Model 接取$inlinecount 資料的共用Model去接資料,沒有就用一般方式即可。
if(arrParams.Select(x => x.ToLower()).Contains("&$inlinecount=allpages") == true)
Select出來轉小寫以後判斷是否包含"&$inlinecount=allpages",因為沒實際測試$filter大小寫是否有區別,所以沒有在取得QueryString的時候就轉。
由組回 Name=Value 時前面有沒有加 & 來決定這邊要不要加。

組合資料來源Api的URL
string.Format("DefauleUrl?name1=val1&name2=val2{0}", string.Join("", arrParams));
string.Join要不要加 & 一樣由前面就決定了,串出來的Url請仔細確認參數部分的格式是不是正確的。

下面是實際範例
public Object Get()
{
    List<string> arrParams = Request.GetQueryNameValuePairs().Where(x => x.Key.StartsWith("$")).Select(x => string.Format("&{0}={1}", x.Key, Uri.EscapeDataString(x.Value))).ToList();
    bool IsAllpages = false;
    string strParams = string.Join("", arrParams);
 string strUrl = string.Format("DefauleUrl?name1=val1&name2=val2{0}", string.Join("", arrParams));
    List<ModelName> Models = new List<ModelName>();
    ODataByApi<ModelName> odateModel = new ODataByApi<ModelName>();
    if (arrParams.Select(x => x.ToLower()).Contains("&$inlinecount=allpages"))
    {
        odateModel = GetData<ODataByApi<ModelName>>(strParams);
        Models = odateModel.Items.ToList();
        IsAllpages = true;
    }
    else
    {
        Models = GetData<List<ModelName>>(strParams);
    }
 //資料處理
    if (Models != null)
    {
        //To do....
    }
 //有$inlinecount=allpages,丟回ODataByAp後return
    if (IsAllpages == true)
    {
        odateModel.Items = Models;
        return odateModel;
    }
 //否則return List
    return Models;
}

參考:
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料)


2014年5月7日 星期三

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料

延續前一篇(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)

當C#去接API來的資料,且有設定$inlinecount,資料格式會變更成:
{
  "Items": [
    {
      //資料1
    },
    {
      //資料2
    },
    {
      //資料3
    }
  ],
  "NextPageLink": null,
  "Count": 3
}
Items存放資料,NextPageLink存放下一頁網址,Count存放數量

無法使用一般Model去接資料,因此需要準備一個共用Model
public class ODataByApi<T>
{
    public ICollection<T> Items { get; set; }
    public string NextPageLink { get; set; }
    public int Count { get; set; }
}

用共用Model,並指定資料Model去接資料即可
JsonConvert.DeserializeObject<ODataByApi<ModelName>>(strJson);


參考:
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)

2014年5月5日 星期一

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount

WebApi提供各種平台取得相關資料,為了滿足各種平台不同的需求(排序、分頁、查詢等等),最直覺的方式就是指定參數給他們使用,再處理各個參數,但這樣就變成不夠彈性,不需要查詢也要傳參數的值,同時加重了前、後端開發人員的麻煩。

使用OData讓前端傳入參數,不需要寫死程式,隨心所欲的完成排序、分頁、查詢等功能。
先來看看OData常用參數:
$top傳回前幾筆資料
$skip跳過幾筆資料
$filter
  • 查詢(where)
    eq-等於、gt-大於、lt-小於、ne-不等於
  • 串連
    and、or
  • $orderby排序
    $inlinecount傳回資料、總筆數、下一頁Url
  • allpages
  • none(預設)
  • 使用方式跟一般傳參數一樣,例如:http://{domain}/api/{controller}/{action}?$top=5&$skip=10
    (更多參數說明請參考官方網站)

    在API的部分,需要設定屬性[Queryable],就可以使用OData。
    備註:
    1.許多文章都說需要使用[Queryable]搭配回傳IQuerable、AsQueryable(),經過測試後,回傳IEnumerable也是可以正常使用的。
    2.如果不想開放所有參數、或有一些限制條件,後端人員可以在[Queryable]設定相關參數以達到目的

    這樣就可以依照各種情況由前端開發人員自行決定需要傳遞那些參數,而後端開發人員只需要專心的處理資料以確保資料正確性即可。


    在測試的時候發現$inlinecount一直沒辦法使用(微軟好像不支援一些參數),這樣前端在做分頁時就不知道資料總筆數,所以我們動手實作一個屬性吧

    public class InlineCountQueryableAttribute : QueryableAttribute
        {
            private static MethodInfo _createPageResult =
                typeof(InlineCountQueryableAttribute)
                .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
                .Single(m => m.Name == "CreatePageResult");
    
            public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
            {
                base.OnActionExecuted(actionExecutedContext);
    
                HttpRequestMessage request = actionExecutedContext.Request;
                HttpResponseMessage response = actionExecutedContext.Response;
    
                IQueryable result;
                if (response.IsSuccessStatusCode
                    && response.TryGetContentValue<IQueryable>(out result))
                {
                    long? inlineCount = request.GetInlineCount();
                    if (inlineCount != null)
                    {
                        actionExecutedContext.Response = _createPageResult.MakeGenericMethod(result.ElementType).Invoke(
                            null, new object[] { request, request.GetInlineCount(), request.GetNextPageLink(), result }) as HttpResponseMessage;
                    }
                }
            }
    
            internal static HttpResponseMessage CreatePageResult<T>(HttpRequestMessage request, long? count, Uri nextpageLink, IEnumerable<T> results)
            {
                return request.CreateResponse(HttpStatusCode.OK, new PageResult<T>(results, nextpageLink, count));
            }
        }
    
    設定屬性由[Queryable]改為[InlineCountQueryable]即可正常使用$inlinecount參數

    參考:
    System.Web.Http.OData 命名空間
    OData官網
    關於IQueryable特性的小實驗
    Web API, OData, $inlinecount and testing


    2014/07/22 KaiYai補充:
    最近無意間發現了Bug,當程式碼出現錯誤產生Exception,Action回前端時依然會進到Attribute內執行OnActionExecuted事件,因為是Exception,所以傳進來的HttpActionExecutedContext.Response會是null,而HttpActionExecutedContext.Exception會是錯誤資訊,如果利用原本程式碼收到的錯誤訊息不會是原本實際發生錯誤的部分,而是會出現OnActionExecuted內的錯誤:
    <Error>
        <Message>發生錯誤。</Message>
        <ExceptionMessage>並未將物件參考設定為物件的執行個體。</ExceptionMessage>
        <ExceptionType>System.NullReferenceException</ExceptionType>
        <StackTrace>
            ...略...
        </StackTrace>
    </Error>
    

    但實際上的錯誤應該是
    <Error>
        <Message>發生錯誤。</Message>
        <ExceptionMessage>輸入字串格式不正確。</ExceptionMessage>
        <ExceptionType>System.FormatException</ExceptionType>
        <StackTrace>
            ...略...
        </StackTrace>
    </Error>
    

    這是因為response已經收到null值,指令沒有判斷到是否為null,就會發生會設定物件的錯誤。
    所以修正條件式加上判斷response != null
    if (response != null && response.IsSuccessStatusCode && response.TryGetContentValue<IQueryable>(out result))

    原本想要直接判斷HttpActionExecutedContext.Exception != null,但想了想,為什麼執行base.OnActionExecuted(actionExecutedContext);沒有發生錯誤呢?所以決定看一下QueryableAttribute的Code,看到下的條件以後,毅然決然的直接照辦,有錯微軟會先被罵
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        ...略...
        HttpResponseMessage response = actionExecutedContext.Response;
    
        if (response != null && response.IsSuccessStatusCode) 
        ...略...
    }
    


    參考:
    QueryableAttribute Source(aspnetwebstack /src/System.Web.Http.OData/QueryableAttribute.cs)

    2014年4月7日 星期一

    ASP.NET MVC 4 Controller 使用委派 ( delegate ) 做更進階的篩選

    委派是事件的基礎。

    將委派與具名方法或匿名方法建立關聯,即可具現化 (Instantiated) 委派。 如需詳細資訊,請參閱具名方法匿名方法
    委派必須以具有相容傳回型別和輸入參數的方法或 Lambda 運算式具現化。 如需方法簽章中可允許之變異數等級的詳細資訊,請參閱委派中的變異數 (C# 和 Visual Basic)。 若要搭配匿名方法使用,就要同時宣告委派以及其相關聯的程式碼。

    以上引用:delegate (C# 參考)

    方便處在於可以在類別上宣告後,即可在各個方法函數內使用,可以依照方法不同,而產生結果也不同,但是只用同一委派 ( delegate ) 實現。

    這裡使用北風資料庫來實作委派,沒有資料庫的話可以參考:Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx )

    在 Controller 中撰寫,傳入訂單編號來搜尋訂單下的訂單細項之產品的名稱,單位價格區間。

    先看看資料庫關聯表:

    先宣告委派,並且決定傳入傳出參數,關鍵字、最小價格、最大價格:
    delegate bool SearchProduct(Order_Details order_detail, string keyword, decimal mixprice, decimal minprice);
    

    類別內宣告委派:
    SearchProduct sp;
    

    最後在 Controller 實作並且使用委派:
    [HttpGet]
    public IEnumerable<Order_Details> SearchProduct(int orderid, string keyword, decimal minprice, decimal maxprice)
    {
        sp = (o, k, min, max) =>
        {
    
            return o.Products.ProductName.Contains(k) ||
                   o.Products.UnitPrice >= min &&
                   o.Products.UnitPrice <= max;
        };
    
        var Order_Details = db.Order_Details
            .Include(x => x.Products)
            .Where(x => x.Orders.OrderID == orderid)
            .ToList()
            .Where(x => sp(x, keyword, minprice, maxprice));
    
        foreach (Order_Details _order_detail in Order_Details)
        {
            _order_detail.Products.Order_Details = null;
        }
    
        return Order_Details; 
    }
    

    執行後呼叫 API - http://localhost:8090/api/Product/SearchProduct?orderid=10285&keyword=Ch&maxprice=18&minprice=17,得到結果:
    [
        {
            "OrderID": 10285,
            "ProductID": 1,
            "UnitPrice": 14.4,
            "Quantity": 45,
            "Discount": 0.2,
            "Orders": null,
            "Products": {
                "ProductID": 1,
                "ProductName": "Chai",
                "SupplierID": 1,
                "CategoryID": 1,
                "QuantityPerUnit": "10 boxes x 20 bags",
                "UnitPrice": 18,
                "UnitsInStock": 39,
                "UnitsOnOrder": 0,
                "ReorderLevel": 10,
                "Discontinued": false,
                "Categories": null,
                "Order_Details": null,
                "Suppliers": null
            }
        }
    ]
    

    為何不在 LINQ 內使用條件判斷? 其實我已經實作過了,會出現「運算式樹狀架構可能不含指派運算子」錯誤,且必須要實例化出來才可以做,以下錯誤程式碼:
    [HttpGet]
    public IEnumerable<Order_Details> SearchProduct(int orderid, string keyword, decimal minprice, decimal maxprice)
    {
    
        Products _product;
    
        var Order_Details = db.Order_Details
            .Include(x => x.Products)
            .Where(x => x.Orders.OrderID == orderid &&
                (
                    (_product = x.Products) != null &&
                    _product.ProductName.Contains(keyword) ||
                    _product.UnitPrice >= minprice ||
                    _product.UnitPrice <= maxprice
                )
    
            ).ToList();
    
        return Order_Details; 
    }
    


    2014年3月26日 星期三

    中文數字轉阿拉伯數字 ( C# 測試版 )

    如何將「六十兆零五十二億三千八百六十二萬六千四百二十五」轉為阿拉伯數字,網路上大部分都是將阿拉伯數字轉為中文數字,所以我就自己來寫一個中文數字轉阿拉伯數字。

    首先要定義一、二、三、....、九的對應,且要定義百、千、億、兆的對應,由於「兆」已經超過 int ( 整數 ) 範圍了,所以在這裡使用 log ( 長整數 ):
    Dictionary<string, long> digit =
        new Dictionary<string, long>() 
        { { "一", 1 }, 
          { "二", 2 }, 
          { "三", 3 }, 
          { "四", 4 }, 
          { "五", 5 }, 
          { "六", 6 }, 
          { "七", 7 }, 
          { "八", 8 }, 
          { "九", 9 } };
    Dictionary<string, long> word =
        new Dictionary<string, long>() 
        { { "百", 100 }, 
          { "千", 1000 }, 
          { "萬", 10000 }, 
          { "億", 100000000 }, 
          { "兆", 1000000000000 } };
    
    Dictionary<string, long> ten =
        new Dictionary<string, long>() 
        { { "十", 10 } }; 
    

    為何要這樣宣告?假設說你的一為壹、二為貳、.....、九為玖,就可以多加幾列
    { "壹", 1 }, 
    { "貳", 2 }, 
    
    ...
    
    { "玖", 9 },
    

    百、千、億、兆 位數字也可以這樣子做,因為中文數字可以混用,甚至可以使用簡體中文,達到最大使用性。

    接下來就是程式部份了,其實規格很簡單,數字由左向右,或加或乘且紀錄,當文字碰到比千還大的文字 ( 萬、億、兆 ),就要將紀錄值

    我使用兩個變數儲存 t_l、 _t_l,_t_l 紀錄該次讀取的文字,先將文字除掉所有零字。

    若為數字,則紀錄於 _t_l;若為十,則看 _t_l 是否為 0,不為 0,則 _t_l 乘上 10,為 0,則 _t_l 為 10;
    若不為數字,則將 _t_l 乘上 文字對應的值;
    若碰上比千位數大的文字 ( 萬、億、兆 ),就要將 _t_l 加上 t_l 再乘文字對應的值。

    最後再把殘餘值加上,輸出結果。

    先看程式碼:
    public long GetChineseNumberToInt(string s)
    {
        long iResult = 0;
    
        s = s.Replace("零", "");
        int index = 0;
        long t_l = 0, _t_l = 0;
        string t_s;
    
        while (s.Length > index)
        {
            t_s = s.Substring(index++, 1);
    
            // 數字
            if (digit.ContainsKey(t_s))
            {
                _t_l += digit[t_s];
            }
            // 十
            else if (ten.ContainsKey(t_s))
            {
                _t_l = _t_l == 0 ? 10 : _t_l * 10;
            }
            // 百、千、億、兆 
            else if (word.ContainsKey(t_s))
            {
                // 碰到千位則使 _t_l 與 t_l 相加乘上目前讀到的數字,
                // 並將輸出結果累加。
                if (word[t_s] > word["千"])
                {
                    iResult += (t_l + _t_l) * word[t_s];
                    t_l = 0;
                    _t_l = 0;
    
                    continue;
                }
                _t_l = _t_l * word[t_s];
                t_l += _t_l;
    
                _t_l = 0;
            }
    
    
        }
        // 將殘餘值累加至輸出結果
        iResult += t_l;
        iResult += _t_l;
    
        return iResult;
    
    }
    

    迴圈每走完一次,數值的變化:
    文字iResultt_l_t_l
    006
    0060
    6000000000000000
    6000000000000005
    60000000000000050
    60000000000000052
    6000520000000000
    6000520000000003
    6000520000000030000
    6000520000000030008
    6000520000000038000
    6000520000000038006
    60005200000000380060
    60005200000000380062
    6000523862000000
    6000523862000006
    6000523862000060000
    6000523862000060004
    6000523862000064000
    6000523862000064002
    60005238620000640020
    60005238620000640025

    這樣看數值變化比較有感覺。

    最後把殘餘值加上,輸出 60005238626425。




    2014年3月24日 星期一

    ASP.NET MVC 4 WebApi -- 利用繼承實作多專案間的部分類別(partial)

    多人開發的專案裡,部分類別(partial)是非常好用的東西,但有個小小的缺點,只能再同一個exe或dll才有作用,如果是參考其他專案的dll,編譯就會失敗
    為了減少相同程式碼出現的頻率,所以改用繼承的方式

    A專案
    namespace A.Models
    {
        public class Field
        {
            public Guid FieldId { get; set; }
            public DateTime CreateOn { get; set; }
            public Guid Creater { get; set; }
            public DateTime UpdateOn { get; set; }
            public Guid Updater { get; set; }
      
            public ICollection Language { get; set; }
        }
    
        public class FieldLanguage
        {
            public Guid FieldLanguageId { get; set; }
            public string Aliases { get; set; }
            public string Notes { get; set; }
            public string Language { get; set; }
      
            public Field Field { get; set; }
        }
    }
    

    B專案(參考A專案)
    namespace B.Models
    {
        public class Field : A.Models.Field
        {
      //放置View所需語系資料
            public string Language_Aliases { get; set; }
            public string Language_Notes { get; set; }
        }
    }
    

    當需要把FieldLanguage放到變數中做處理時,就會發生"找不到類型或命名空間名稱'FieldLanguage'"的錯誤

    B的Model追加FieldLanguage,因為欄位相同,所以一樣繼承A的FieldLanguage
    public class FieldLanguage : A.Models.FieldLanguage { }
    

    雖然解決了找不到類別的問題,但是也多了"類型'A.Models.FieldLanguage'不能隱含轉換為'B.Models.FieldLanguage'"的錯誤
    原來繼承裡面所關聯的類別是原本所指定的,不會因為繼承的專案有相同類型名稱而轉換過去,所以繼承後要再把關聯加回去
    public ICollection FieldLanguage { get; set; }
    

    這邊會有一個警告,"'B.Models.FieldLanguage'隱藏了繼承的成員'A.Models.FieldLanguage'。如果是刻意要隱藏,請使用new關鍵字"
    因為名稱是一樣的,所以編譯器會自動把父類別的屬性或方法隱藏掉
    PS:範例是用屬性,所以用new的方式,如果是方法,請盡量使用override(可參考new和override的差異與目的)
    public new ICollection FieldLanguage { get; set; }
    

    FieldLanguage當然也要參考回Field
    public new Field Field { get; set; }
    

    最後B的類別如下:
    namespace B.Models
    {
        public class Field : A.Models.Field
        {
            public string Language_Aliases { get; set; }
    
            public string Language_Notes { get; set; }
    
            public new ICollection FieldLanguage { get; set; }
        }
    
        public class FieldLanguage : A.Models.FieldLanguage
        {
            public new Field Field { get; set; }
        }
    }
    
    以上就可以完成多專案的部分類別,各專案本身當然也是可以繼續使用部分類別(partial)

    問:為什麼要加回關聯?
    答:因為在Model只要做一次,後面資料處理就可以很輕鬆,當然不加也是可以,只是資料處理還要做型態轉換,如果這個Model很多地方會使用,那光型態轉換就有得忙了

    參考:
    new和override的差異與目的
    區分 abstract、virtual、override 和 new

    2014年3月10日 星期一

    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 一個Form同時上傳Data與File

    在閱讀此文章前,請先參考
    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 送出表單 ( Submit Form )
    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 動態複製表單並送出
    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 上傳檔案 ( FileUpload )

    為了避免網路或其他因素造成只成功上傳某些資料,所以需要將Form跟File上傳到同一個WebApi做資料儲存

    同時上傳資料與檔案,Controller無法自動區分Form跟File丟到指定的Model裡面,所以我們需要對傳上來的資料做處理。
    每次傳上來的資料類別不同,做成共用的Method讓大部分的狀況都可以直接使用,而且自動轉成我們所指定的型別。

    先把Request.Form傳進去,用Keys跑迴圈把Value放到Dictionary去,因為我們需要自動轉換型別,先把Dictionary轉成Json格式,然後讓JsonConvert.DeserializeObject<T>去幫我們做型別轉換的處理。
    備註:Key跟欄位名稱要一樣,多的Key不會被放到Model,沒有資料的欄位會是null。
    public T getSingleFormData<T>(NameValueCollection Form)
    {
      List<T> Data = new List<T>();
      Dictionary<string, object> tmpData = new Dictionary<string, object>();
      foreach (var item in Form.AllKeys)
      {
        tmpData.Add(item, Form[item]);
      }
      if (tmpData.Count > 0)
      {
        T formData = JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(tmpData));
        Data.Add(formData);
      }
      return Data.FirstOrDefault();
    }
    

    看完單筆資料接收,當然有可能資料是多筆,這邊需要做傳過來的Key做切割,然後判斷這是第幾筆資料,所以要對AllKeys做排序,避免傳過來的順序亂掉導致轉換後是錯誤的資料。
    public List<T> getMultiFormData<T>(NameValueCollection Form)
    {
      List<T> Datas = new List<T>();
      Dictionary<string, object> tmpData = new Dictionary<string, object>();
      string Seq = string.Empty;
      foreach (var item in Form.AllKeys.OrderBy(x => x))
      {
        string[] formName = item.Split('.');
        string formSeq = formName[0].Substring(1, formName[0].Length - 2);
        if (string.IsNullOrEmpty(Seq) == false && Seq != formSeq)
        {
          if (tmpData.Count > 0)
          {
            T formData = JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(tmpData));
            Datas.Add(formData);
          }
          tmpData.Clear();
        }
        Seq = formSeq;
        tmpData.Add(formName[1], Form[item]);
      }
    
      if (tmpData.Count > 0)
      {
        T formData = JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(tmpData));
        Datas.Add(formData);
      }
      return Datas;
    }
    

    接下來做檔案的接收,因為直接把檔案存到資料庫,所以用Stream去存放。
    public IDictionary<string, object> getFileData(HttpFileCollection Files)
    {
      Dictionary<string, object> FilesStream = new Dictionary<string, object>();
      foreach (var item in Files.AllKeys)
      {
        HttpPostedFile File = Files[item];
        if (string.IsNullOrEmpty(File.FileName) == false)
        {
          Dictionary<string, object> tmpFile = new Dictionary<string, object>();
          tmpFile.Add("FileName", File.FileName);
          tmpFile.Add("Stream", File.InputStream);
          FilesStream.Add(item, tmpFile);
        }
      }
      return FilesStream;
    }
    

    Method都寫完,開始動手寫資料處理的部分。
    public HttpResponseMessage Post()
    {
      var httpRequest = HttpContext.Current.Request;
      //多筆
      List<Products> AllProducts = getMultiFormData<Products>(httpRequest.Form);
      /*
      Code
      */
    
      //單筆
      Products Products = getSingleFormData<Products>(httpRequest.Form);
      /*
       Code
       */
    
      //檔案
      IDictionary<string, object> Files = getFileData(httpRequest.Files);
      foreach (var item in Files)
      {
        Dictionary<string, object> tmpFile = (Dictionary<string, object>)item.Value;
       /*
        Code
        */
      }
      return Request.CreateResponse(HttpStatusCode.OK);
    }
    

    David Kuo
    為什麼要傳參數,而不在Method直接取Request?

    答:
    如果從Method直接取Request,這樣不夠彈性,變成其他NameValueCollection、HttpFileCollection的資料要取就會沒辦法使用,然後也有可能在建立共用物件時發生Request未初始化的問題。
    參考:
    ASP.NET MVC 4 Web Api 回傳HttpResponseMessage遇到 System.ArgumentNullException: Value cannot be null. Parameter name: request

    2013年11月11日 星期一

    GC.SuppressFinalize 的用法

    GC.SuppressFinalize ,通常使用在自己實作的 Dispose 使用之後,但我不太懂為什麼還要再使用 GC.SuppressFinalize ?

    Dispose(true);
    GC.SuppressFinalize(this);
    

    後來我在網路上找到一段很有趣的解釋:

    dispose告诉这个实体:哥不要你了,你可以去死了。
    GC.SuppressFinalize(true); 这就是告诉系统,看到死尸了,让他去清理一下

    其實光是 GC.SuppressFinalize(true) 就已經是個錯誤,正確用法是 GC.SuppressFinalize(this)

    在 MSDN 上的解釋為「 要求系統不要為指定物件呼叫完成項 」,備註為「

    這個方法會在物件標頭中設定位元,當系統呼叫完成項時會檢查這個位元。 obj 參數必須是這個方法的呼叫端。 如果 obj 沒有完成項,呼叫 SuppressFinalize 方法沒有作用。

    實作 IDisposable 介面的物件會從 IDisposable.Dispose 方法呼叫這個方法,以免記憶體回收行程在不需要 Object.Finalize 的物件上呼叫它。



    最後附上正確解釋,以下引用來自 [C#]Effective C# 條款十八:實現標準Dispose模式 的解釋:

    具有解構子的物件其在被垃圾收集器回收處理時,會先被放入解構佇列之中,再交由另一個專門處理解構動作的執行緒去做解構的動作,當解構的動作完成,該物件又會被放回原來的佇列等待垃圾收集器的回收,因此其性能上的耗費會比沒有解構子的物件還來的多。由於IDisposable在實作上會習慣加入解構子做為保險措施,防止類別的使用者忘記叫用Dispose方法,造成資源的洩漏。故在釋放完資源後,我們應該隨即在後呼叫GC.SuppressFinalize,告知垃圾收集器該物件的解構動作跳過不處理。

    2013年9月25日 星期三

    ASP.NET C# EXCEL 檔案上傳不儲存檔案讀取資料

    在以往的檔案上傳,都要先將上傳的檔案存到伺服器端的某個目錄,還必須要將檔案命名成獨立的名稱,不然同時有可能會發生有人檔案上傳失敗。

    但是如果不存實體檔案,存到一個暫存空間 ( MemoryStream ),再將資料轉換成 DataTable 或者是 IDataReader ,最後資料讀取出來或傳入 GridView 使用。

    1.

    首先先下載 ClosedXML:http://closedxml.codeplex.com/releases/view/110822 並引入參考。

    2.

    撰寫一個類別使用 ClosedXML 寫常用的方法到時方便使用。 ( 以下是參考程式碼,可依照狀況不同調整 )
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Data;
    using System.Data.OleDb;
    using System.IO;
    using ClosedXML.Excel;
    
    namespace FileHanding
    {
        //ClosedXML Documentation: http://closedxml.codeplex.com/documentation
        public class FileHanding
        {
            public MemoryStream TransferDataTableToExcel(DataTable dt)
            {
                var wb = new XLWorkbook();
                wb.Worksheets.Add(dt);
                MemoryStream ms = new MemoryStream();
                wb.SaveAs(ms);
                return ms;
            }
    
            public DataTable TransferExcelToDataTable(byte[] file)  
            {
                Stream fileStream = new MemoryStream(file);
                var workbook = new XLWorkbook(fileStream);
                var xlWorksheet = workbook.Worksheet(1);
                return TransferExcelToDataTable(xlWorksheet);
            }
    
            public DataTable TransferExcelToDataTable(byte[] file, string sheetName)
            {
                Stream fileStream = new MemoryStream(file);
                var workbook = new XLWorkbook(fileStream);
                var xlWorksheet = workbook.Worksheet(sheetName);
                return TransferExcelToDataTable(xlWorksheet);
            }
    
            public DataTable TransferExcelToDataTable(string filePath)
            {
                var workbook = new XLWorkbook(filePath);
                var xlWorksheet = workbook.Worksheet(1);
                return TransferExcelToDataTable(xlWorksheet);
            }
    
            public DataTable TransferExcelToDataTable(string filePath, string sheetName)
            {
                var workbook = new XLWorkbook(filePath);
                var xlWorksheet = workbook.Worksheet(sheetName);
                return TransferExcelToDataTable(xlWorksheet);
            }
    
            private DataTable TransferExcelToDataTable(IXLWorksheet xlWorksheet)
            {
                var datatable = new DataTable();
                var range = xlWorksheet.Range(xlWorksheet.FirstCellUsed(), xlWorksheet.LastCellUsed());
    
                int col = range.ColumnCount();
                int row = range.RowCount();
    
                // add columns hedars
                datatable.Clear();
    
                for (int i = 1; i <= col; i++)
                {
                    IXLCell column = xlWorksheet.Cell(1, i);
                    datatable.Columns.Add(column.Value.ToString());
                }
    
                // add rows data   
                int firstHeadRow = 0;
                foreach (var item in range.Rows())
                {
                    if (firstHeadRow != 0)
                    {
                        var array = new object[col];
                        for (int y = 1; y <= col; y++)
                        {
                            array[y - 1] = item.Cell(y).Value;
                        }
                        datatable.Rows.Add(array);
                    }
                    firstHeadRow++;
                }
                return datatable;
            }
    
            
            public IDataReader TransferExcelToIDataReader(byte[] file)
            {
                Stream fileStream = new MemoryStream(file);
                var workbook = new XLWorkbook(fileStream);
                var xlWorksheet = workbook.Worksheet(1);
                return TransferExcelToIDataReader(xlWorksheet);
            }
    
            public IDataReader TransferExcelToIDataReader(byte[] file, string sheetName)
            {
                Stream fileStream = new MemoryStream(file);
                var workbook = new XLWorkbook(fileStream);
                var xlWorksheet = workbook.Worksheet(sheetName);
                return TransferExcelToIDataReader(xlWorksheet);
            }
    
            public IDataReader TransferExcelToIDataReader(string filePath)
            {
                var workbook = new XLWorkbook(filePath);
                var xlWorksheet = workbook.Worksheet(1);
                return TransferExcelToIDataReader(xlWorksheet);
            }
    
            public IDataReader TransferExcelToIDataReader(string filePath, string sheetName)
            {
                var workbook = new XLWorkbook(filePath);
                var xlWorksheet = workbook.Worksheet(sheetName);
                return TransferExcelToIDataReader(xlWorksheet);
            }
    
            private IDataReader TransferExcelToIDataReader(IXLWorksheet xlWorksheet)
            {
                var datatable = new DataTable();
                var range = xlWorksheet.Range(xlWorksheet.FirstCellUsed(), xlWorksheet.LastCellUsed());
    
                int col = range.ColumnCount();
                int row = range.RowCount();
    
                // add columns hedars
                datatable.Clear();
    
                for (int i = 1; i <= col; i++)
                {
                    IXLCell column = xlWorksheet.Cell(1, i);
                    datatable.Columns.Add(column.Value.ToString());
                }
    
                // add rows data   
                int firstHeadRow = 0;
                foreach (var item in range.Rows())
                {
                    if (firstHeadRow != 0)
                    {
                        var array = new object[col];
                        for (int y = 1; y <= col; y++)
                        {
                            array[y - 1] = item.Cell(y).Value;
                        }
                        datatable.Rows.Add(array);
                    }
                    firstHeadRow++;
                }
                return datatable.CreateDataReader();
            }
    
    
    
            public MemoryStream TransferDataTableToCsv(DataTable dt)
            {
                MemoryStream ms = new MemoryStream();
                StreamWriter result = new StreamWriter(ms, Encoding.UTF8);
    
                //Header
                for (int i = 0; i < dt.Columns.Count; i++)
                {
                    result.Write(dt.Columns[i].ColumnName);
                    result.Write(i == dt.Columns.Count - 1 ? "\n" : ",");
                }
    
                //Content
                foreach (DataRow row in dt.Rows)
                {
                    for (int i = 0; i < dt.Columns.Count; i++)
                    {
                        result.Write(row[i].ToString());
                        result.Write(i == dt.Columns.Count - 1 ? "\n" : ",");
                    }
                }
                
                return ms;
            }
    
            public DataTable TransferCsvToDataTable(string strFilePath)
            {
                string strFileName = Path.GetFileName(strFilePath);
                string strFileDirectory = Path.GetDirectoryName(strFilePath);
                string strConn = string.Format(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\;Extended Properties='Text;HDR=Yes;'", strFileDirectory);
                string strSQL = string.Format("SELECT * FROM [{0}]", strFileName);
                OleDbDataAdapter adapter = new OleDbDataAdapter(strSQL, strConn);
                DataTable dt = new DataTable();
                adapter.Fill(dt);
                return dt;
            }
    
        }
    }
    

    3.

    介面上,只需要用一般的 input 加上 runat="server" 即可,利用 Postback 將檔案讀取。以下是介面圖:

    再看看程式碼快照:

    4.

    在上傳檔案時,將檔案轉換成 byte[],並傳入已經寫好的類別 ( FileHanding ) 方法進行轉換,以下為參考程式碼:
    using FileHanding;
    
    ...
    ...
    ...
    ...
    ...
    ...
    
    protected void btnImport_Click(object sender, EventArgs e)
    {
        if (IsPostBack)
        {
            if (file.PostedFile != null)
            {
                FileHanding fh = new FileHanding();;
                string strErrorMessage = string.Empty;
                var postedFile = file.PostedFile;
                int iDataLength = postedFile.ContentLength;
                byte[] bData = new byte[iDataLength];
                bool bIsVerify = true;
                postedFile.InputStream.Read(bData, 0, iDataLength);
    
                if (txtSheetName.Text.Trim() == string.Empty)
                    dt = fh.TransferExcelToDataTable(bData);
                else
                    dt = fh.TransferExcelToDataTable(bData, txtSheetName.Text);
    
                /* 以下再做資料處理 */ 
    
    
            }
        }
    }
    
    




    2012年5月3日 星期四

    Xpath 語法 - 使用 HtmlAgilityPack 於 C#

    XPath即為XML路徑語言(XML Path Language),它是一種用來確定XML文檔中某部分位置的語言。

    XPath基於XML的樹狀結構,提供在資料結構樹中找尋節點的能力。起初 XPath 的提出的初衷是將其作為一個通用的、介於XPointer與XSL間的語法模型。但是 XPath 很快的被開發者採用來當作小型查詢語言。

    引用 & 參考:XPath 語言XPath AxesXML Path Language (XPath)

    XPATH 基本語法

    • para selects the para element children of the context node

      para 選擇 para 子元素的本文節點


    • * selects all element children of the context node

      * 選擇所有子元素的本文節點


    • text() selects all text node children of the context node

      text() 選擇所有 text 子結點的本文節點


    • @name selects the name attribute of the context node

      @name 選擇所有 name 屬性的本文節點


    • @* selects all the attributes of the context node

      @* 選擇所有屬性的本文節點


    • para[1] selects the first para child of the context node

      para[1] 選擇所有第一個 para 元素的本文節點
    • para[last()] selects the last para child of the context node

      para[last()]選擇所最後一個 para 元素的本文節點


    • */para selects all para grandchildren of the context node

      */para 選擇所有 para 子孫的本文節點


    • /doc/chapter[5]/section[2] selects the second section of the fifth chapter of the doc

      /doc/chapter[5]/section[2] 選擇 doc 下的第五個 chapter 下的第二個 section 節點


    • chapter//para selects the para element descendants of the chapter element children of the context node

      chapter//para 選擇所有的父節點為chapter元素的para元素


    • //para selects all the para descendants of the document root and thus selects all para elements in the same document as the context node

      //para 選擇所有為 para 元素
    • //olist/item selects all the item elements in the same document as the context node that have an olist parent

      //olist/item 選擇所有父節點為 olist 元素的 item 元素


    • . selects the context node

      . 選擇當前節點


    • .//para selects the para element descendants of the context node

      .//para
    • 選擇當前節點的所有 para 子元素
    • .. selects the parent of the context node

      .. 選擇當前節點的父節點


    • ../@lang selects the lang attribute of the parent of the context node

      ../@lang 選擇名為 lang 的所有属性


    • para[@type="warning"] selects all para children of the context node that have a type attribute with value warning

      para[@type="warning"] 選擇所有 title 元素,且這些元素擁有值為 warning 的 lang 属性


    • para[@type="warning"][5] selects the fifth para child of the context node that has a type attribute with value warning

      para[@type="warning"][5] 選擇所有 title 元素,且這些元素擁有值為 warning 的 lang 属性的第五個節點


    • para[5][@type="warning"] selects the fifth para child of the context node if that child has a type attribute with value warning

      para[5][@type="warning"] 選擇所有 title 元素的第五個節點,且這個元素擁有值為 warning 的 lang 属性


    • chapter[title="Introduction"] selects the chapter children of the context node that have one or more title children with string-value equal to Introduction

      chapter[title="Introduction"] 選擇所有 chapter 元素,且其中的 title 元素的值等於 Introduction


    • chapter[title] selects the chapter children of the context node that have one or more title children

      chapter[title] 選擇所有 chapter 元素,且其中有 title 元素的值


    • employee[@secretary and @assistant] selects all the employee children of the context node that have both a secretary attribute and an assistant attribute


      employee[@secretary and @assistant] 選擇所有 employee 元素,且其中有 assistant 和 secretary 屬性的元素



    XPATH 座標軸

    • ancestor 選擇當前節點的所有先輩(父、祖父等)
    • ancestor-or-self 選擇當前節点的所有先辈(父、祖父等)以及當前節點本身
    • attribute 選擇當前節點的所有屬性
    • child 選擇當前節點的所有子元素
    • descendant選擇當前節點的所有後代元素(子、孫等)
    • descendant-or-self 選擇當前節點的所有後代元素(子、孫等)以及當前節點本身
    • following 選擇文檔中當前節點的结束標籤之後的所有節點
    • namespace 選擇當前節點的所有命名空間節點
    • parent 選擇當前節點的父節點
    • preceding 選擇文檔中當前節點的開始標籤之前的所有節點
    • preceding-sibling 選擇當前節點之前的所有同级節點
    • self 選擇當前節點

    使用範例

    要擷取某些節點,必須要先觀察節點的獨特性質,或者是共通屬性,我通常都是使用 firefox 的外掛來觀察,IE 和 chrome 都有這種元件。
    這次的例子我們在 google 上輸入 xpath,想要擷取前 10 筆網頁的標題,就先觀察它的網頁結構。

    可以利用上面解說的方法,依照網頁結構來擷取,在程式中寫入以下 C# 程式碼:
    HtmlWeb web;
    HtmlDocument doc;
    HtmlNodeCollection nodes;
    string xp_title = string.Empty;
    
    
    web = new HtmlWeb();
    doc = web.Load("http://www.google.com/search?hl=en&q=xpath&oq=XPath");
    
    xp_title = @"//h3[@class=""r""]";
    nodes = doc.DocumentNode.SelectNodes(xp_title);
    
    foreach (HtmlNode node in nodes)
    {
        Console.WriteLine(node.InnerText);
    }

    執行結果如下:

    比起 regular expression 輕鬆的是,它是以網頁結構去擷取,而 regular expression 是依照匹配字串做比對,所以在比對上 regular expression 比較有難度。
    但是,在特殊情況下,還是需要利用 regular expression 做精密的解析。


    回C#目錄
    回首頁

    2012年4月11日 星期三

    SQL 自製類別

    這個 SQL 類別為我個人設計並撰寫,可能設計的不夠完整,就請廣大的觀眾多多指教囉!

    若要引用請註明來源,歡迎取用。







    使用範例:
    private SQL sql;
    
    protected void Page_Load(object sender, EventArgs e)
    {
        sql = new SQL("NorthwindConnectionString", 
            SQLConnectionType.SettingsName);
    
        Hashtable ht = new Hashtable();
        ht.Add("eID", "1");
    
        gv.DataSource = sql.ExecuteDataTable(
            "SELECT * FROM Employees WHERE EmployeeID=@eID ", ht);
        gv.DataBind();
        
    
    }

    類別程式碼如下:
    public enum SQLConnectionType
    {
        ConnectionString,
        SettingsName
    };
    
    /// <summary>
    /// SQL 的摘要描述
    /// </summary>
    public class SQL
    {
        private DataTable dt;
    
        private SqlConnection connection;
        private SqlCommand command;
        private SqlDataAdapter adapter;
        public SqlDataReader reader { get; set; }
        private SqlTransaction tran;
        public bool isError { get; set; }
        public string errorMessage { get; set; }
    
        /// <summary>
        /// 
        /// </summary>
        public SQL()
        {
    
        }
    
        /// <summary>
        /// 初始化資料庫連線
        /// </summary>
        public SQL(string strConnection, SQLConnectionType type)
        {
            Connection(strConnection, type);
        }
    
        /// <summary>
        /// 資料庫連線
        /// </summary>
        /// <param name="strConnection">連接字串</param>
        /// <param name="type">資料庫連線方式</param>
        public void Connection(string strConnection, SQLConnectionType type)
        {
            connection = new SqlConnection();
            command = new SqlCommand();
    
            switch (type)
            {
                case SQLConnectionType.ConnectionString:
                    
                    connection.ConnectionString = strConnection;
                    command.Connection = connection;
                    
                    break;
    
                case SQLConnectionType.SettingsName:
    
                    connection.ConnectionString =
                        ConfigurationManager.ConnectionStrings[strConnection].
                        ConnectionString;
                    command.Connection = connection;
    
                    break;
            }
        }
    
        /// <summary>
        /// 執行資料庫語法
        /// </summary>
        /// <param name="query">指令</param>
        public void Execute(string query)
        {
            command.CommandText = query;
            connection.Open();
    
            isError = false;
            try
            {
                // 開始執行資料庫交易
                tran = connection.BeginTransaction();
                command.Transaction = tran;
                command.ExecuteNonQuery();
                tran.Commit();
    
            }
            catch (Exception ex)
            {
                // 失敗則 Rollback
                tran.Rollback();
    
                errorMessage = ex.Message;
                isError = true;
            }
            finally
            {
                connection.Close();
            }
        }
    
        /// <summary>
        /// 執行資料庫語法
        /// </summary>
        /// <param name="query">指令</param>
        /// <param name="ht">參數</param>
        public void Execute(string query, Hashtable ht)
        {
            SetParameters(ht);
            Execute(query);
        }
    
        /// <summary>
        /// 傳回結果第一行第一列之資料
        /// </summary>
        /// <param name="query">指令</param>
        /// <returns></returns>
        public object ExecuteScalar(string query)
        {
            object obj = string.Empty;
    
            command.CommandText = query;
            connection.Open();
    
            isError = false;
            try
            {
                obj = command.ExecuteScalar();
            }
            catch (Exception ex)
            {
                errorMessage = ex.Message;
                isError = true;
            }
            finally
            {
                connection.Close();
            }
    
            return obj;
        }
    
        /// <summary>
        /// 傳回結果第一行第一列之資料
        /// </summary>
        /// <param name="query">指令</param>
        /// <param name="ht">參數</param>
        /// <returns></returns>
        public object ExecuteScalar(string query, Hashtable ht)
        {
            SetParameters(ht);
    
            return ExecuteScalar(query);
        }
    
    
        /// <summary>
        /// 傳回 DataTable
        /// </summary>
        /// <param name="query">指令</param>
        /// <returns></returns>
        public DataTable ExecuteDataTable(string query)
        {
            dt = new DataTable();
            command.CommandText = query;
            
            isError = false;
            try
            {
                adapter = new SqlDataAdapter(command);
                adapter.Fill(dt);
            }
            catch(Exception ex)
            {
                errorMessage = ex.Message;
                isError = true;
            }
    
            return dt;
        }
    
        /// <summary>
        /// 傳回 DataTable
        /// </summary>
        /// <param name="query">指令</param>
        /// <param name="ht">參數</param>
        /// <returns></returns>
        public DataTable ExecuteDataTable(string query, Hashtable ht)
        {
            SetParameters(ht);
    
            return ExecuteDataTable(query);
        }
    
        /// <summary>
        /// 傳回 SqlDataReader
        /// </summary>
        /// <param name="query">指令</param>
        public SqlDataReader ExecuteReader(string query)
        {
            command.CommandText = query;
    
            reader = command.ExecuteReader();
    
            return reader;
        }
    
        /// <summary>
        /// 傳回 SqlDataReader
        /// </summary>
        /// <param name="query">指令</param>
        /// <param name="ht">參數</param>
        /// <returns></returns>
        public SqlDataReader ExecuteReader(string query, Hashtable ht)
        {
            SetParameters(ht);
            return ExecuteReader(query);
        }
    
        /// <summary>
        /// 查詢資料是否存在
        /// </summary>
        /// <param name="query">指令</param>
        /// <returns></returns>
        public bool Exists(string query)
        {
            object obj;
            bool isExists = false;
            int iResult = 0;
    
            command.CommandText = query;
            connection.Open();
    
            isError = false;
            try
            {
                obj = command.ExecuteScalar();
    
                if (int.TryParse(Convert.ToString(obj), out iResult))
                {
                    if (iResult > 0)
                        isExists = true;
                }
            }
            catch (Exception ex)
            {
                errorMessage = ex.Message;
                isError = true;
            }
            finally
            {
                connection.Close();
            }
    
            return isExists;
        }
    
        /// <summary>
        /// 查詢資料是否存在
        /// </summary>
        /// <param name="query">指令</param>
        /// <param name="ht">參數</param>
        /// <returns></returns>
        public bool Exists(string query, Hashtable ht)
        {
            SetParameters(ht);
            return Exists(query);
        }
    
        /// <summary>
        /// 將資料庫連線開啟或關閉
        /// </summary>
        /// <param name="strStatus">狀態字串(open or close)</param>
        public void CallConnection(string strStatus)
        {
            switch (strStatus.ToLower())
            {
                case "open":
                    if (connection.State == ConnectionState.Closed)
                        connection.Open();
                    break;
                case "close":
                    if (connection.State == ConnectionState.Open)
                        connection.Close();
                    break;
    
            }
        }
    
        /// <summary>
        /// 設定參數
        /// </summary>
        /// <param name="ht">參數</param>
        private void SetParameters(Hashtable ht)
        {
            command.Parameters.Clear();
            foreach (DictionaryEntry de in ht)
                command.Parameters.AddWithValue(Convert.ToString(de.Key),
                Convert.ToString(de.Value));
        }
    }

    回C#目錄
    回首頁

    2012年2月17日 星期五

    AJAX學習筆記

    市面上常用的瀏覽器很多

    設計一個AJAX網站

    為了讓所有使用者都可以正常瀏覽

    就需要判別目前使用的瀏覽器

    用其所定義的XMLHttpRequest物件

    if (window.XMLHttpRequest)
    {// 如果是 Mozilla, Safari,...
      http_request = new XMLHttpRequest();
    } 
    else if (window.ActiveXObject)
    {// 如果是 IE
      try
      {// IE 又分成新版和舊版的,其處理方式也不同
       // 新版的 IE
        http_request = new ActiveXObject("Msxml2.XMLHTTP");
      }
      catch (e)
      {
        try
        {// 舊版的 IE
          http_request = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (e) {}
      }
    }
    



    建立好XMLHttpRequest物件之後

    開始跟web server要資料

    http_request.open("GET/POST", "資源URL", true/false);
    http_request.send([參數]);
    

    參數說明:
    open()
    一、GET/POST是看web server支援情況
    二、資源只可存取於同一個web server,指定其完整的URL
    三、是否非同步,true可在資源傳送完成前執行其他動作

    send()
    GET直接帶null
    http_request.send(null);
    

    如果用POST
    需定義MIME類型及URL參數
    http_request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    http_request.send("[id1]=[value1]&[id2]=[value2]&[id3]=[value3]&...");
    



    既然是非同步

    那麼在發送我們要哪些資源前

    需要先做好"完成資源傳送"的事件處理函數

    http_request.onreadystatechange = function()
    {
      // 要執行的事件函數
    };
    



    可以用readyState得知送出的http_request處理狀況
    0 未初始化:未呼叫open()
    1 載入中:未呼叫send()
    2 載入完成:server以接收,可以取得content header
    3 資源傳送中:可以取得部分傳送完成的資源
    4 完成:資源已傳送完成

    以及states得知處理結果
    請檢視HTTP Status Codes ( 狀態碼 )

    if (http_request.readyState == 4)
    {
      if (http_request.status == 200)
      {
        // 可以依照我們的需求來處理資源了
      }
      else
      {
        // 自行決定要如何處理錯誤的情形
      }
    }
    



    資源都傳送完成了

    就可以開始接收與處理資源

    http_request 提供兩種方式來存取資料:
    http_request.responseText:回傳的資料為字串,需利用字串的處理函數。
    http_request.responseXML :回傳的資料為XMLDocuemnt,可利用JavaScript的DOM APIs來存取這份XML物件。

    回aspnet目錄
    回html目錄
    回C#目錄
    回首頁

    2012年2月10日 星期五

    HTTP Status Codes ( 狀態碼 )

    最近因為大量擷取網路資料,需要利用 Http Request 類別擷取資料,在這期間不斷切換 Proxy server ( 代理伺服器 ),但是會碰到一些 Http Status Codes,除了 200 - 成功 之外,其他都看不懂,所以 google 了一下,列出 Http Status Codes 供大家參考。

    HTTP CODES - 100-101

    100 - Continue ( 繼續 )
    Tells the client that the first part of the request has been received and that it should continue with the rest of the request or ignore if the request has been fulfilled.

    101 - Switching Protocols ( 切換通訊協定 )
    Tells the client that the server will switch protocols to that specified in the Upgrade message header field during the current connection.



    HTTP CODES 200-206

    200 - OK ( 確定。 用戶端要求成功 )
    The request sent by the client was successful.

    201 - Created ( 已建立 )
    The request was successful and a new resource was created.

    202 - Accepted ( 已接受 )
    The request has been accepted for processing, but has not yet been processed.

    203 - Non-Authoritative Information ( 非授權資訊 )
    The returned meta information in the entity-header is not the definitive set as available from the origin server.

    204 - No Content ( 無內容 )
    The request was successful but does not require the return of an entity-body.

    205 - Reset Content ( 重設內容 )
    The request was successful but the User-Agent should reset the document view that caused the request.

    206 - Partial Content ( 部分內容 )
    The partial GET request has been successful.



    HTTP CODES 300-307

    300 - Multiple Choices
    The requested resource has multiple possibilities, each with different locations.

    301 - Moved Permanently ( 要求的網頁已經永久改變網址 )
    The resource has permanently moved to a different URI.

    302 - Found ( 物件已移動,並告知移動過去的網址 )
    The requested resource has been found under a different URI but the client should continue to use the original URI.

    303 - See Other ( 通知 Client 連到另一個網址去查看上傳表單的結果(POST 變成 GET),當使用程式作網頁轉向時,會回應此訊息 )
    The requested response is at a different URI and should be accessed using a GET command at the given URI.

    304 - Not Modified ( 未修改 )
    The resource has not been modified since the last request.

    305 - Use Proxy ( 要求的網頁必須透過 Server 指定的 proxy 才能觀看 ( 透過 Location 標頭 ) )
    The requested resource can only be accessed through the proxy specified in the location field.

    306 - No Longer Used ( (未使用) 此代碼僅用來為了向前相容而已 )
    Reserved for future use.

    307 - Temporary Redirect ( 暫時重新導向 )
    The resource has temporarily been moved to a different URI. The client should use the original URI to access the resource in future as the URI may change.



    HTTP CODES 400-417

    400 - Bad Request ( 錯誤的要求 )
    The syntax of the request was not understood by the server.

    401 - Not Authorised ( 拒絕存取 )
    The request needs user authentication

    402 - Payment Required
    Reserved for future use.

    403 - Forbidden ( 禁止使用 )
    The server has refused to fulfill the request.

    404 - Not Found ( 找不到 )
    The document/file requested by the client was not found.

    405 - Method Not Allowed ( 用來存取這個頁面的 HTTP 動詞不受允許 (方法不受允許) )
    The method specified in the Request-Line is not allowed for the specified resource.

    406 - Not Acceptable ( 用戶端瀏覽器不接受要求頁面的 MIME 類型 )
    The resource requested is only capable of generating response entities which have content characteristics not specified in the accept headers sent in the request.

    407 - Proxy Authentication Required ( 需要 Proxy 驗證 )
    The request first requires authentication with the proxy.

    408 - Request Timeout
    The client failed to sent a request in the time allowed by the server.

    409 - Conflict
    The request was unsuccessful due to a conflict in the state of the resource.

    410 - Gone
    The resource requested is no longer available and no forwarding address is available.

    411 - Length Required
    The server will not accept the request without a valid Content-Length header field.

    412 - Precondition Failed ( 指定條件失敗 )
    A precondition specified in one or more Request-Header fields returned false.

    413 - Request Entity Too Large ( 要求的實體太大 )
    The request was unsuccessful because the request entity is larger than the server will allow.

    414 - Request URI Too Long ( 要求 URI 太長 )
    The request was unsuccessful because the URI specified is longer than the server is willing to process.

    415 - Unsupported Media Type ( 不支援的媒體類型 )
    The request was unsuccessful because the entity of the request is in a format not supported by the requested resource for the method requested.

    416 - Requested Range Not Satisfiable ( 無法滿足要求的範圍 )
    The request included a Range request-header field, and not any of the range-specifier values in this field overlap the current extent of the selected resource, and also the request did not include an If-Range request-header field.

    417 - Expectation Failed ( 執行失敗 )
    The expectation given in the Expect request-header could not be fulfilled by the server.



    HTTP CODES 500-505

    500 - Internal Server Error ( 內部伺服器錯誤 )
    The request was unsuccessful due to an unexpected condition encountered by the server.

    501 - Not Implemented ( 標頭值指定未實作的設定 )
    The request was unsuccessful because the server can not support the functionality needed to fulfill the request.

    502 - Bad Gateway ( Web 伺服器在作為閘道或 Proxy 時收到無效的回應 )
    The server received an invalid response from the upstream server while trying to fulfill the request.

    503 - Service Unavailable ( 服務無法使用。 這是 IIS 6.0 專用的錯誤碼 )
    The request was unsuccessful to the server being down or overloaded.

    504 - Gateway Timeout ( 閘道逾時 )
    The upstream server failed to send a request in the time allowed by the server.

    505 - HTTP Version Not Supported ( 不支援的 HTTP 版本 )
    The server does not support or is not allowing the HTTP protocol version specified in the request.

    引用: HTTP Status Codes
    網頁開發人員應了解的 HTTP 狀態碼


    回目錄
    回首頁

    2012年1月22日 星期日

    HtmlAgilityPack 遇到擷取亂碼網頁的解決方法



    解決方法就是:

    1. 先到 http://htmlagilitypack.codeplex.com 下載 HtmlAgilityPack 原始碼。

    依照下面方式點擊即可下載。



    2. 下載完畢,解壓縮檔案到 \HtmlAgilityPack.1.4.0.Source\HtmlAgilityPack\HtmlWeb.cs 的 1466 行左右,有一段程式碼:

    Encoding respenc = !string.IsNullOrEmpty(resp.ContentEncoding)
                           ? Encoding.GetEncoding(resp.ContentEncoding)
                           : null;

    修改改成以下所示:

    Encoding respenc;
    
    if ((resp.ContentEncoding != null) && (resp.ContentEncoding.Length > 0))
    {
        respenc = System.Text.Encoding.GetEncoding(resp.ContentEncoding);
    }
    else if ((resp.CharacterSet != null) && (resp.CharacterSet.Length > 0))
    //根據Content-Type中獲取的charset  
    {
        if (string.Compare(resp.CharacterSet, "UTF-8", true, 
        System.Globalization.CultureInfo.InvariantCulture) == 0)
            respenc = System.Text.Encoding.GetEncoding("UTF-8");
        else if (string.Compare(resp.CharacterSet, "BIG5", true, 
        System.Globalization.CultureInfo.InvariantCulture) == 0)
            respenc = System.Text.Encoding.GetEncoding("BIG5");
        else if (string.Compare(resp.CharacterSet, "iso-8859-1", 
        true, System.Globalization.CultureInfo.InvariantCulture) == 0)
            respenc = System.Text.Encoding.GetEncoding("UTF-8");
        else
            respenc = System.Text.Encoding.GetEncoding(resp.CharacterSet);
    }
    else
    {
        respenc = System.Text.Encoding.GetEncoding("UTF-8");
    }

    再將整個專案重建,參考到 \HtmlAgilityPack.1.4.0.Source\HtmlAgilityPack\bin\Debug\HtmlAgilityPack.dll 來使用。

    回目錄
    回首頁



    2011年12月31日 星期六

    HTML Parser ( 剖析器 ) - HtmlAgilityPack ( HAP )

    網頁探勘 (Web Mining) 是使用資料探勘技術由網際網路的文件及服務中發現並擷取出隱含的資訊。

    我在網頁探勘中下了許多功夫,例如之前發表一篇 Regular Expression 與 C# Regex 教學 的文章,頗受歡迎。

    由於 Regular Expression 並不好學,需要熟悉 Regular Expression 語法邏輯與組合去對應網頁語法與結構,才能將有用資料擷取出來分析,甚至放到資料庫儲存,已供往後做成報表研究。但是如果網頁結構改變,整個 Regular Expression 語法都要改變,而每一條語法並不是這麼好下,可能要花一些時間去瞭解網頁結構的狀況而定。

    HtmlAgilityPack 套件,類似視窗程式的 WebBrowser 一樣,先載入它的網站狀況,它可以讓剖析鬆散格式 HTML 的工作就像剖析 XML 一樣簡單,只要依照他網頁結構找出它的 xpth,便可輕鬆擷取出資料。

    首先,先到Html Agility Pack下載 dll,目前版本為 1.4.0。

    在程式碼中,先加入這個 dll,接著再 *.cs 引用:
    using HtmlAgilityPack;

    目前我拿奇摩首頁 ( http://tw.yahoo.com/ ),做擷取範例,抓出所有連結的 html 語法。以下為範例程式碼:

    string url = "http://tw.yahoo.com/";
    
    HtmlWeb web = new HtmlWeb();
    HtmlDocument doc = web.Load(url);
    
    HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a");
    
    foreach (HtmlNode node in nodes)
    {
        Console.WriteLine(node.OuterHtml);
    }

    xpath 語法參考:XPath 語法

    這樣變輕輕鬆鬆地擷取出所有連結。

    歡迎大家來討論,目前我也正在學習中。

    回目錄
    回首頁