2018年6月18日 星期一

文字雲 word cloud in python

這篇也是因為工作上需要,所以用Python進行文字雲的分析,想要從常用字詞,進行資料的分類,預期的結果除了統計常用字詞的次數以外,還有好像很炫但要細心看的文字雲圖式,這次做的是【以五月天30首熱門歌曲文字分析,統計出常用字詞,並以五月天 Mayday 的logo視覺化】 (如有侵權請告知,我很怕被吉...)


插個題外話,寫著寫這(其實是貼著貼著),覺得應該寫一系列叫做【輕輕鬆鬆複製貼上 寫Python】,Keep Calm and Copy Paste Series#2 (Keep calm 系列的典故請看這,看個blog順便學學歷史,做個好深宅)

圖片來源: The Keep Calm-o-matic

回到主題,如果要文字分析產生文字雲,有很多工具或網站可以直接使用,但如果想要用自己的字典跟特殊的停用字句,還是需要自己動手做。既然Python也不難寫,就來動手做一下,以後也好累積自己的字典跟停用詞句,以後也方便自己做分析分類。
這次是用五月天 在KKBOX 2018/6 最流行的30首歌詞分析,目的是知道哪些字詞是常用的語句,在應用上滿多種的,如果是分析PTT,那就等於是特定主題的輿情分析,其他領域可以是文章分類等非結構化的文章判斷,那就來看一看程式跟結果吧:

    • Step 1 Import 與定義變數


# coding: utf-8
# 部分參考: https://ithelp.ithome.com.tw/articles/10192043 
#       非常感謝王選仲大大
# 部分參考 http://blog.fukuball.com/ru-he-shi-yong-jieba-jie-ba-zhong-wen-fen-ci-cheng-shi/
#       非禪感謝林志傑 Fukuball大大
# 字典來自 https://git.oschina.net/fxsjy/jieba
#       真的是做最好的中文分词组件
#

from PIL import Image
import matplotlib.pyplot as plt
from wordcloud import WordCloud, ImageColorGenerator
import jieba
import numpy as np
from collections import Counter

    • Step 2 利用Jieba切字
#要分析的來源,這個範例是五月天30首歌
text_from_file_with_apath = open(r'lyrics.txt', "r",encoding="utf-8").read()

#設定字典
jieba.set_dictionary('dict.txt.big')
#設定自訂的字典,譬如五月天的 "動次"
jieba.load_userdict(r'userdict_mayday.txt')

#設定停用詞,譬如唱歌會用到的oh,喔
with open(r'stopWord_mayday.txt', 'r', encoding='utf8') as f:  
    stops = f.read().split('\n') 

#
#開始段詞與排序,沒錯就這麼簡單就做完了
#
terms = [t for t in jieba.cut(text_from_file_with_apath, cut_all=True) if t not in stops]
sorted(Counter(terms).items(), key=lambda x:x[1], reverse=True) 

    • Step 3 分析完後丟給文字雲繪圖
#中文繪圖需要中文字體,請自己從windows font目錄抓
#微軟正黑體
font = r'msjh.ttc'
#想要文字雲出現的圖示
mask = np.array(Image.open(r"mayday_mask.png"))

#背景顏色預設黑色,改為白色
#mark改用五月天的皇冠
#其他參數請自行參考wordcloud
my_wordcloud = WordCloud(background_color="white",mask=mask,font_path=font,collocations=False, width=2400, height=2400, margin=2)  
my_wordcloud.generate_from_frequencies(frequencies=Counter(terms))

#產生圖片
plt.figure( figsize=(20,10), facecolor='k')
plt.imshow(my_wordcloud,interpolation='bilinear')
plt.axis("off")
plt.tight_layout(pad=0)
#顯示用
plt.show()

#存檔用
#plt.savefig("Mayday_Wordcloud.png")



mayday_mask是五月天的皇冠logo,當作遮罩,套上文字雲後,最後的結果就類似這樣,

看來 "一天"、"快樂"、"相信"、"人生"、"回憶"、"愛"是五月天常用的文字。

如果朋友們有更簡單的方式也歡迎跟小弟交流教學相長~~
如果文字內容或圖片有侵權也請告知

#Python
#資料分析
#Word cloud
#文字雲
#Sky is limit

2018年5月26日 星期六

網路爬蟲開發 - 氣象局自動雨量站-雨量觀測 in Python



  • 前言

近期因為工作上需要氣象局的雨量資料,所以開始找相關的介接程式,從政府資料開放平台 (https://data.gov.tw/) 上面共有2個可以介接的資料來源

  1. 氣象局API: https://data.gov.tw/dataset/9177 
  2. 環保署Opendata: https://data.gov.tw/dataset/7879 

實際上來源都是氣象局,但環保署Opendata的資料集已經整理過是平面的資料表,直接用python pandas 可以直接 read_csv or read_json方式直接讀取,但也許是介接的人太多,效能很差,常常要等很久且抓不到資料,如果要作認真的應用(真的要拿來每小時分析或計算)會很困難,因此這一篇是以氣象局的API方式介接並寫入資料庫。


  • 介接方式
透過氣象局Open Data API取得資料,資料經過整理後,再寫入MS SQL Server

  • 前提
必須在氣象局 Open Data 網站 (http://opendata.cwb.gov.tw) 註冊帳號並取得授權碼,如果有很多資料集的應用,需要詳細研讀解氣象局呼叫方式。

    • Step 1 Import 與定義變數
# coding: utf-8

# 
# Purpose: 自中華民國氣象局 Open Data 取得自動雨量站 每10分鐘雨量資料 
# Remark: 解析json資料有參考 https://github.com/leafwind/cwb-cache 非常感謝
#
import logging
import pandas as pd
import requests
from datetime import datetime
import sqlalchemy

#氣象局API網址
CWB_URL = 'http://opendata.cwb.gov.tw/api/v1/rest/datastore/'
#自動雨量站 資料集代號
DATA_ID = 'O-A0002-001'
#氣象局 OPEN DATA 授權碼,需要自行修改
AUTH_KEY = 'CWB-OOOOOOOO-OOOO-OOOO-OOOO-OOOOOOOOOOOO'
#目的地 SQL Server 的連結字串,需要自行修改
SQL_CONNECTION_STRING = "mssql+pyodbc://[account]:[password]@[SqlServer]:1433/[Database]?driver=SQL+Server+Native+Client+11.0"
#目的地 SQL Server Table名稱,需要自行修改
DEST_TABLE = 'tb_Rainfall_API'
    • Step 2 取得API的資料集
這段用function方式模組化程式,感謝 leafwind的大作 https://github.com/leafwind/cwb-cache

def get_data_from_cwb(data_id, auth_key, params={}):
   '''limit, offset, format, locationName, elementName, sort'''
   logging.info('getting data from CWB...')

   dest_url = CWB_URL + '{}'.format(data_id)
   r = requests.get(dest_url, headers={'Authorization': auth_key})
   params_list = ['{}={}'.format(key, params[key]) for key in params]
   params_str = '?' + '&'.join(params_list)
   dest_url += params_str
   logging.debug('dest_url: {}'.format(dest_url))
   
   if r.status_code != 200:
       logging.error('r.status_code: {}'.format(r.status_code))
       return None

   data = r.json()
   
   if data.get('success') != 'true':
       return None
   return data
    • Step 3 解析API資料成為Dataframe
雖然概念上很簡單,但是因為解析那段有點麻煩,所以沒辦法很直覺的爬回來就準備寫入資料庫,所以需要轉換,可以直接呼叫這個網址當範例 (只查兩站,速度比較快),我們需要的資料是藍色框框,records\location 向下的資料,其中 weatherElement 是雨量的值。呼叫的function 如下。


def parse_json_to_dataframe(data):
   columns = ['stationId','locationName','lat','lon', 'obstime','ELEV','RAIN','MIN_10','HOUR_3','HOUR_6','HOUR_12','HOUR_24','NOW']
   df = pd.DataFrame(columns=columns)
   dataDic = {}
   locations = data['records']['location']
   row = -1
   for l in locations:
       row = row + 1
       dataDic['stationId'] = l['stationId']
       dataDic['locationName'] = l['locationName']
       dataDic['obstime'] = l['time']['obsTime']
       dataDic['lat'] = l['lat']
       dataDic['lon'] = l['lon']
       factors = l['weatherElement']
       for f in factors:
           factor_name = f['elementName']
           dataDic[factor_name] = f['elementValue']
       for key in dataDic.keys():
           df.loc[row,key] = dataDic[key]
       

   return df 


    • Step4 主程式:取得資料、解析成為資料表的格式、寫入資料庫
if __name__ == '__main__':
    json_data = get_data_from_cwb(DATA_ID, AUTH_KEY, {})
    df = parse_json_to_dataframe(json_data)
    df['InsertDatetime']=datetime.now()

# In[9]:

#寫入資料庫

engine = sqlalchemy.create_engine(SQL_CONNECTION_STRING)
conn = engine.connect()

df.to_sql(DEST_TABLE,engine,if_exists='append')

#確認資料庫結果
rs = conn.execute('SELECT TOP 10 * FROM ' + DEST_TABLE +' with (nolock) ORDER BY InsertDatetime desc ')
_result = pd.DataFrame(rs.fetchall())
_result.columns = rs.keys()
conn.close()

_result


  • Step5 進入資料庫的資料處理
對於這個資料集,會有一些額外的資料面要處理,如雨量的監測值-998轉成0等等,小弟認為這些是資料庫端處理會比較方便,譬如寫一個 stored procedure 或 trigger處理資料,這些就給各位依照自己的需求再調整了。



進入資料庫後,就容易進行後續的應用、計算與分析了!
如果朋友們有更簡單的方式也歡迎跟小弟交流切磋~~

#Python
#Crawler
#氣象局
#環保署
#雨量
#Sky is limit

2017年11月20日 星期一

網路爬蟲開發 - AQIStudy Crawler

因為近期有需要去中國網站抓空品資料分析的需求,他是一個城市一個城市個別放的https://www.aqistudy.cn/historydata/

備註 AQI 是計算空氣品質指標的方式,詳細請看我國環保署的網站

為了比較方便處理,因此通通抓到資料庫再用Query或丟到Excel處理。處理方式是利用python去爬爬爬,放到MS SQL,再用SQL的方式進行分析比對,任務很簡單,但處理起來需要一些技巧。

1. 抓資料

#import需要的工具
import pandas as pd
import sqlite3
import urllib.parse
import sqlalchemy, pyodbc
from datetime import datetime
from selenium import webdriver

#取得所有城市,程式清單請自己整理成TXT
def get_city_set():
    str_file = r'city.txt'
    fp = open(str_file,'rb')
    city_set = list()
    for line in fp.readlines():
        city_set.append(str(line.strip(),encoding='utf-8'))
    return city_set
city_set = get_city_set()
#開始爬資料
base_url = 'https://www.aqistudy.cn/historydata/monthdata.php?city='
#SQLServer的帳密,請自己改
engine = sqlalchemy.create_engine("mssql+pyodbc://[帳號]:[密碼]@[主機]:1433/[資料庫名]?driver=SQL+Server+Native+Client+11.0")
conn = engine.connect()

for city in city_set:
    weburl = ('%s%s' % (base_url,urllib.parse.quote(city.encode('utf-8'))) )
    print(weburl)
    driver = webdriver.PhantomJS()
    driver.get(weburl)
    dfs = pd.read_html(driver.page_source,header=0)[0]
    driver.quit()
    dfs['City']=city
    dfs['InsertDatetime']=datetime.now()
    print(city,"\t",len(dfs))
    dfs.to_sql("[表格名]",engine,if_exists='append')

conn.close()
#判斷結果
engine = sqlalchemy.create_engine("mssql+pyodbc://[帳號]:[密碼]@[主機]:1433/[資料庫名]?driver=SQL+Server+Native+Client+11.0")
conn = engine.connect() rs = conn.execute('SELECT [City],count(*) FROM tbAQI with (nolock) group by [City]') for row in rs: print(row) conn.close()

2. 分析
 用Pivot就可以方便資料分析人員在Excel做逐時的比對

select * from 
(
 select city,'PM25' as [Object],[月份],[pm2.5] from [表格名]
 group by city,[月份],[pm2.5]
)as S
PIVOT
(
 sum([pm2.5])
 for mon in ([2014-01],[2014-02],[2014-03],[2014-04],[2014-05],[2014-06],[2014-07],[2014-08],[2014-09],[2014-10],[2014-11],[2014-12],
  [2015-01],[2015-02],[2015-03],[2015-04],[2015-05],[2015-06],[2015-07],[2015-08],[2015-09],[2015-10],[2015-11],[2015-12],
  [2016-01],[2016-02],[2016-03],[2016-04],[2016-05],[2016-06],[2016-07],[2016-08],[2016-09],[2016-10],[2016-11],[2016-12],
  [2017-01],[2017-02],[2017-03],[2017-04],[2017-05],[2017-06],[2017-07],[2017-08],[2017-09],[2017-10])
)AS P

貼到Excel就長這樣,可以多城市一起比對分析,方便!


#Python
#Crawler
#AQI
#AQIStudy
#Sky is limit

2014年1月31日 星期五

Agile Contract Type (III) Comparison

在前兩篇整理了幾種合約類型,最後一篇是整理比較表。對於合約,就像「好吃」的定義一樣,每個人都可以講一套自己的理論,所以小弟找了一個甲方資深同學,請他加上甲方對各類合約的想法,還好這篇在 Blog 發表,不是論文,要不然就只好叫他對自己的內容 "出來面對" ╮(╯_╰)╭。除了甲方的想法外,加上自己的一些想法,合約的目的是促進甲乙方的合作,對於可能發生的風險可以有一個雙方都可以接受的方案,而不是造成甲乙雙方互不信任,各位捧有,Agile Manifesto 講的 "與客戶合作重於合約協商 Customer Collaboration over Contract Negotiation"  說的真是有道理阿~~

合約類型 小弟 Comment 資深甲方's Comment
固定價格合約(Fixed price) 範圍夠明確的專案才比較適合。
現在台灣的軟體專案大部分都簽這種類型的,但是台灣的甲方(特別是私人企業端的)對「完成」的定義會無限上綱,一不小心就是相看兩厭..甲方覺得乙方難配合,乙方寧願不收驗收期的款項,儘早開始往下一個乙方專案。
至於固定價格合約(Fixed price) 我的心得 就是合約階段 一定要跟老闆想要的階段 整合在一起(老闆想要的就是我們這種小小承辦人績效來源)
有一次有一個一年大案子 基本上分 三個階段 要完成 但第一階段 佔50%工作量 卻被要求 2個月內要完工 意思就是合理工期至少6 個月的 工期 卻被要求縮短30% 完工, 我只好使出殺手鐧: 我跟廠商說你幫我做到 我一次直接先給你2個階段費用 ) 反正後面還有第三階段 二三階段 廠商看在第三階段款 也不太可能不做。那一次經驗 果然廠商在我 [威脅利誘]下 又早2天完成第一階段上線…。
時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap) 適用一定範圍內的解決問題
解決個Bug、改個什麼確定的程式功能,這種就滿適合的。看甲方是時間優先還是預算優先,高手貴一點,資淺的便宜一點,有時候解決問題比錢重要...
至於時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap)跟固定價格合約(Fixed price)  我通常運用在 不同性致的廠商或專案上:

如果這個需求非常急 就是那種 7/25 跟你說 8/1 一定要上,如果不上公司會遭受多少莫大損失,而手上廠商對需求範圍熟悉度很低,根本時間不讓我做到最簡單的需求確認 /SA 分析/SD分析 /CODING 測試 ….我就會直接選擇時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap) 直接跟對方談 我跟你買5天人CODING 人力 你派人來 我直接跟你說改哪邊 做什麼?(用嘴巴作完需求確認 /SA 分析/SD分析….)  基本上我還會多利用目標成本合約精神,我會跟廠商說 我會至少買5 天 ,三天你的人就做完 那是你的本事 不會少給你2 天。 那一次廠商 就派個高手來 4HR 作完5 天要做的事情!!!!
漸進式交付合約(Incremental Delivery) 長時間、大範圍的專案應該都考慮用這種專案
事實上大範圍應該先切分階段個別簽約,對雙方都會比較好,先有一個小專案磨合比較容易瞭解,但是有時候就一定要一次發一大包的話,那應該切多個小階段,每個階段做完考慮還需不需要繼續。如果只為了消化預算而繼續開發價值低(使用頻率低 或是 為了未來一兩年後可能發生需求..)的系統,那才真的是浪費錢...
 一般軟體外包客製和約,買方公司不太可能簽漸進式交付合約(Incremental Delivery 因為如果這個需求 至少要買5箱蘋果 你不可能跟老闆說 我們先跟對方簽 10買10箱蘋果  然後再看看要用到幾箱 因為老闆絕對會質疑承辦人 阿 你是不是 搞不清楚需求 不然 幹嘛抓不準?(承辦人寧願買貴 不願買錯…買錯是要負政治責任!!!!)
目標成本合約(Target Cost Contract) 如果甲乙雙方已經建立信任的基礎,對於軟體專案,應該多多考慮採用這種合約,激勵甲方也讓乙方提早使用到有價值的軟體。 目標成本合約(Target Cost Contract) 也不太可能 同樣道理 如果只要花 100 元就可以買到 5 箱蘋果 老闆不會允許 你 如果提前一個月收到蘋果 少付對方 20元,老闆只會要求: 你就是給我提前一個月辦完事情 而且我只願出80 元 ,(小弟 曾經有一次還遇到 老闆不只80 不願出 只願給 50 元  因為 老闆的朋友跟他說 你買貴了…直接硬砍50%)



總結小弟甲乙方都做過,個人觀察,這幾年,台灣的甲方因為陸續導入ERP,被外商訓練過,現在對軟體專案的進行方式都比較熟悉,不會像以前一樣只用價低者得標、又愛凹、結案又愛挑東挑西不給結案,現在比較能接受不同方式。
如果看官你是甲方承辦人,小弟建議你可以依照不同的專案內容,選擇適合的合約方式;
如果看官你是乙方,為了專案執行順利,請多想想可以用什麼合約方式可以讓乙方可以在對的時間獲得價值!
說來說去好像還是在說錢,錢在採購合約上絕對是關鍵,但絕對不是最大關鍵, 比如我們公司所有承辦人就很討厭跟大軟體公司合作 尤其那種在台成立20幾年的…..因為1他不缺錢 2.溝通都派業務 (業務都直接找老闆搞定我們這種小承辦員) 業務都年輕貌美說話甜 3. 作事太沒彈性,沒彈性出事時等於逼承辦人什麼都自己扛,等於逼承辦人去跳淡水河…..。4.如果真不幸,大公司還養打官司法務,如果案子搞到打官司 那承辦人 早就跳N次淡水河。

實務上其實很難 一招打片天下無敵手,不同CASE要運用的手法往往不同,良好的買賣關係,其實就跟人際關係一樣要有點黏又不能太黏, 而有些關係還得花時間花力氣去培養,比如如果配合還不錯公司,我會選擇犧牲一時換得長久信任(出小事要幫廠商扛 不要什麼事都要廠商吃下來 總一天 他會要你整個吞回去…….)。



各位看官如果有你自己一套的採購原則或是經驗、案例的分享,也歡迎留言或寄信給我。累積越多合約案例


小弟的敏捷式專案的採購領域整理到這邊,算是已經解答了自己的疑問,再來的主題,是怎麼樣做都不對的績效考評,在敏捷專案領域又怎麼看..











2013年7月29日 星期一

Agile Contract Type (II) - Incremental and Target Cost

接續著上一篇的主題,這一篇則整理另外 2種Agile合約,在國外的討論或是書上會看到的,這兩種在台灣就不是這麼常聽到了:
  • 漸進式交付合約(Incremental Delivery) (1)(2):這種合約會設置好幾個檢查點,每個檢查點可以讓甲方決定還要不要繼續下去,不繼續的話,已經完成的功能也是能運作的軟體,而不是一堆做到一半的程式碼。這種方式是比較適合Agile Project Management,因為每個 Iteration 交出的成品都是可運作的軟體。譬如一年的專案,可以在每一季設一個檢查點,如果乙方的Agile Team每個 Iteration 是 2 週,每個 Iteration 都交付一些重要性高的、可以用的軟體功能,6個 Iteration 時(也就是3個月),甲方就可以決定還要不要繼續下去,如果想要更經常的檢視完成品,也可以雙方討論後,修改檢查點的時間。
  • 目標成本合約(Target Cost Contract) (3 對細節有興趣的朋友,可以花點時間看):這種合約是 Toyota 跟供應商長期簽訂的合約類型,上課的時候也是講說 Agile Project Management 建議簽這樣的合約類型。這種合約先由甲乙雙方決定一個目標金額,專案開始後,則依照比例甲方付款給乙方,而如果乙方提前完成目標,則兩方共享節省的金額,而如果超過目標金額,則兩方共同承擔懲罰。而獎賞或懲罰的比例則事先訂定下來,如果想拿大比例的獎賞,那就要承擔大比例的懲罰。這種合約類型用圖解釋比較清楚:


    • 專案的基本數字:
      • 專案總金額:500 萬,額外準備100萬,總專案的最高費用(Cap)是600萬
      • 簽約時的付款:不先付任何款
      • 結案的付款:結案時給50萬
      • 每月工資:50萬
      • 預計工時:10個月
    • 專案 3種狀況(正常完成、提前、延誤)的付款進度如圖:



    • 正常進度:紅線,每月固定50萬,最後第10個月後到500萬,沒有獎勵也沒有懲罰。
    • 提前:綠色的線,假設提前在第8個月做完的話,可以領到
      • 8個月的400萬
      • 結案時可以領的50萬
      • 激勵金額 (目標的500萬-完工400萬)*50%=50萬,全部加起來也是500萬。在這個情況下,甲方可以提早開始使用系統帶來的效益,而乙方可以早領錢、早結案,專案人員可以慶祝(在台灣軟體開發專案能夠提前,真是難得啊...)並準備投入下一個專案。
    • 延後:在到 10th個月後,11th個月起,甲乙方各付一半(因為獎金也是甲乙各一半),所以11th個月起,每月只能算25萬,一直到550萬為止,所以到12th月後,累計就已經550萬,加上結案可以收到的50萬,就已經到頂(600萬)了,這時候乙方就必須不領錢直到做完為止,雖然看起來只有乙方在suffer,但是實際上甲方一直沒系統可用,這樣造成的損失也是甲方要承擔的懲罰。
    • 因為Target Cost Contract 比較特殊,可能有一些實行上的細節要考慮,除了看 (3) 以外,也可以看這篇 Agile Journal 2005年的 Selling agile: target-cost contracts ,講到非常實務上的細節。
小弟在整理這些合約類型的過程中,如果是乙方,可能需要先取得甲方的信任,在第2次、第3次的專案中,再開始用新的合約類型,比較不會引起甲方的過度懷疑,小弟認為做專案就是信任大考驗,在雙方互信的狀況下,怎麼樣都好談,要是雙方無信任,那就很難再繼續下去....
下一篇則要來個整理表了,比較各專案類型的適用對象,與陰謀論的想一下甲乙方會承擔的風險。

參考

2013年7月27日 星期六

Agile Contract Type (I) - Fixed Price and T&M


這個圖是你的合約關係嗎?

在上Agile Project Management的課程時,一直想跟實際工作情形連起來,連到採購的時候,心中有的大問號:現在一般常見的系統開發合約(開發一個系統,分3或4期付款:簽約、系統設計完成、開發完成、結案) ,跟以 Iteration 為進行方式的Agile Project,要怎麼對應的起來? 用Agile 開發的話,不把全部的SA/SD文件都寫完啊,那要怎麼付款跟請款 ?
上完課後,為了一解心中的疑惑,開始蒐集 Agile Contract 的類型,這一系列分3篇,算是對整理後的筆記,先整理目前常見的合約類型共4種,最後再加上一個比較表,說明適用的時機與缺點。

這一篇先講兩種常見的固定價格合約(Fixed Price and Fixed Scope)與工時與材料(Time and Material)類型的合約:
  • 固定價格合約(Fixed price):是目前最常見的軟體專案發包方式,這種合約通常有固定的總金額與專案範圍,通常付款是分為:
    • (1)合約簽約付一筆
    • (2)中間階段0~N次付款:通常可以分 "系統分析文件交付"、"系統設計文件交付"、"系統開發完成交付"、"系統通過使用者驗收測試(UAT)" 等等不同階段
    • (3)專案結案付一筆
    • 譬如一個中型專案,乙方為了公司營運金流的順暢,希望分 4 次付款,所以請款的時候會是 "合約簽約"、"系統分析、系統設計文件交付與確認"、"通過UAT"、"專案結案",這樣的合約配合瀑布式開發方式是很合適的,各階段都能搭配上。
    • 在Agile Project Management時,並不會一開始完整的分析整個系統,用 固定價格的合約 就會覺得有點卡卡,譬如甲方要求 "SA, SD文件確認" 付款時,乙方不知道要怎麼產出 超大本的SA/SD文件 去給甲方確認,所以出現其他比較符合Agile的合約類型。但是,如果甲方說,只能發包這樣的合約(組織要求或...),乙方是可以應用一些觀念,讓合約比較能符合Agile開發方式,譬如偷偷引入漸進式交付合約的想法(Incremental Delivery),把Release當作階段 而不把SA/SD文件完成當作階段 (完成子系統#1,2當作第一階段、完成子系統#3,4,5 當作第二階段)。
    • Scrum 大師Jeff Sutherland 提出的 2個重要的合約的觀念 (1) ,如果必須要承接/發包固定價格的合約,可以加入到合約中,對甲乙雙方都會有幫助的 (小弟不喜歡用"雙贏", 講出雙贏的人通常都不會說自己贏比較多...)
    • Money for nothing 金錢無用 :甲方可以在任一個 Iteration 後結束專案。評量的標準是, 如果甲方認為後續要做的價值不如已經完成的價值(i.e 已經完成的功能已經能達到所需要,因為Agile 是從高優先順序開始開發) 甲方將會付給乙方剩下合約的20%的金額。( 金錢無用 -> 不想付錢買不會用到的功能..) 
    • Change for free 改變免費:雖然講免費,但實際是,當有改變的時候,Product Owner 在 Iteration Planning 前或中提出新需求,Agile Team 評估 Story Point,確定要做的話,Product Owner把不重要的功能或相等Story Point的功能的移出 Product Backlog,做一些 TradeOff,甲方的高優先需求得以滿足,乙方一樣做那麼多Story Point 的工作,以這個方式來歡迎改變!

  • 時間與材料合約/有上限的時間與材料合約( T&M / T&M with cap):跟台灣俗稱的的"買人力"的想法很接近,可以想成"買整組人"在甲方駐點(通常還是要駐一下, out of sight out of mind, XD )開發所需要的系統,這種方式就是做多久就付多少金額。因為時間都是乙方估計,所以風險都在甲方,也因此有變種的,是加上上限的T&M(Capped T&M)合約,這個上限是雙方同意後的金額,通常甲方應該要加上鼓勵條款,鼓勵乙方早日完成。(不知為何,在台灣的甲方,能夠準時驗收與準時付款,對乙方就是一種激勵...) (2)

下一篇將整理的是漸進式的與目標成本的合約。

參考

2013年7月13日 星期六

[Engineering Practice] 軟體開發的測試-單元測試 / Software Development Testing - Unit Testing

以前只要講到系統功能要測試,通常就會想到找 PM、SA或直接給客戶進行人體測試,結果就會發生一使用功能就 Error 的悲劇... 。今年去上 Agile Project Management 時, 講到強調軟體開發的Extreme Programming時,特別強調測試,在 "XP 規則" ( The Rules of Extreme Programming原文 )中,有一組規則是講測試的,貼上來給大家震撼一下:
  • All code must have unit tests. 所有程式碼都要有單元測試 !!!!!
  • All code must pass all unit tests before it  can be released. 所有程式碼通過單元測試後才能release !
  • When a bug is found tests are created. 如果有發現 bug 時,就要回去寫測試 !!!
  • Acceptance tests are run often and the score is published.  經常執行驗收測試,並且必須公布量化指標!!
對XP Unit Test 有興趣的朋友,可以再看這一篇: Unit Test ..

既然單元測試是這麼重要,在上完Agile的課後,小弟開始學習在.Net Framework 要怎麼做單元測試,這一篇算是介紹,也是對自己學習的筆記,我使用的工具是 C# 與 Visual Studio 2012,使用 Java 的朋友,你一定能找到對應的工具與平台,這年頭, M$ 會推出某個功能,都是被 Java Community 逼出來的..ㄎㄎ... 廢話不多說,我用問題與回答來說明單元測試:

1. 單元測試到底是指甚麼?  =>預期結果等於測試結果
  • 簡單說,單元測試是對程式的最基本的單位進行測試 ( 最基本單位是要看你自己認定,小弟是非常程序導向的開發人員 i.e. old ,我的通常是 Function ),確認程式能夠如預期的執行。小弟的心得是,因為要單元測試,會強迫自己開始規劃自己的程式,讓 function 真正能比較像一個 function 該做的事,不會通通塞到一起才來拼命 debug line by line (還記得以前老師教的高聚合低耦合嗎?... )
  • 在單元測試會做的三件事:這跟人體測試時的邏輯是滿相同的,假設你要呼叫一個兩數相除的 function,會有3 步驟
    • Step (1) 先準備呼叫的資料,如 100除以5,所以你會把100跟5先準備好,這個步驟就是 "安排測試資料(Arrange)"
    • Step (2) 開始呼叫Function,並取得結果,譬如這個function可正常呼叫並回傳20,這個步驟就是 "執行測試(Act)"
    • Step (3) "判斷結果(Assert)其實在提供資料去測試時,你會有預期的結果,所以從第2步取得結果後,就會跟你預期的結果進行比對,以判斷兩數相除的Function是否如果預期執行,譬如 100/5=20,function回傳結果也是20,所以你就可以判斷function是否運作正常了!
    • 下圖是我寫測試一個上傳檔案的 Function 是否正確的單元測試程式,其實跟真正要呼叫該 Function 的程式會很類似,所以其實單元測試寫完,就可以copy/paste到正是用的程式,一魚兩吃阿!


2. 要如何開始做單元測試 ? =>打開你的好朋友(?!) Visual Studio
  • 這個問題跟使用的程式語言與開發工具非常相關了,我用Visual Studio 2012來當範例,基本操作在網路上已經很多教學怎麼做了 (如果跟我一樣從未接觸過,可以參考這一篇 MSDN,照著做一次會有感覺...) ,步驟大致是:
    • (1)在你目前開發的"方案"中,加入一個 "單元測試專案"
    • (2)在新的單元測試專案中,增加"參考" 開發的專案
    • (3)在新的單元測試專案中,開始寫Arrange, Act, Assert的程式。 請注意的是,在一個單元測試中,可以放多筆測試資料,預設是只要一個測試未通過,就會當作不成功,可以多放幾筆確定 function 真的跟預期的結果相同 (該有回傳就回傳,該有Exception 就會有 Exception...)
  • 寫完後,可以直接按 "測試總管" 的 "全部執行",就會知道是否通過單元測試了! 在測試的過程中,再回去改程式,我覺得這個過程是最大的收穫 !

3. 要如何確認自己做夠多的單元測試? => 計算程式碼涵蓋範圍
  • 還記得剛才的XP Test Rule "所有程式碼都要有單元測試 !",所以要如何判斷自己有涵蓋夠多的程式? 你可以使用Visual Studio 2012的  "分析程式碼涵蓋範圍"功能 ,馬上就可以看到涵蓋結果。至於你的目標要設為多少,可能要由你的 developer team 自行在definition of done 中定義。 
  • 如果你有引用Web Service 或 WCF,預設在計算程式碼涵蓋範圍時,會把這些外部參考也會當作範圍,這樣分母就會很大,涵蓋率就會變低,看了就不爽! 這時你可以自己加上一個設定檔(參考這一篇 Customizing Code Coverage Analysis ,最下方有範例檔案),並且在裡面增加排除Web Service/WCF,這樣就不會把外部的算進去了! 想要直接下載的可以到這邊下載,加到你的測試專案中,並在 "測試"->"測試設定"->"選取測試執行檔"使用這設定。
    • 加入後設定檔案後,要自己指定


    • 設定檔案中,如果不想納入Web Service/WCF,這邊要加上排外的設定。設定完再執行一次 "分析程式碼涵蓋範圍" 就會看到變化囉 !



4. 要如何準備單元測試的多筆測試資料? => 資料驅動的測試方式
  • 如果你的developer team有專職的測試人員或是你的Product Owner/Customer 願意準備測試資料,這時你就可以使用 資料驅動的測試方式 (Data-driven test) 。這方式可以用 CSV, EXCEL, Database 當作資料來源,而在單元測試時,就可以取得這些資料,進行多次的測試,太棒啦,測試可也切成資料與邏輯 !  
  • 使用 data-driven 測試有幾個眉角需要注意的
    • (1)資料上,準備時要有輸入的參數,還有預期的結果,譬如大家都熟悉的 Excel ,我有一個function要驗證帳號密碼是否正確,所以有3個欄位:帳號,密碼,預期結果,類似下圖

    • (2)資料上,檔案要自己加入倒測試專案中,並且在 "複製到輸出目錄" 要選 "永遠複製"
    • (3)程式上,要自己加上 using System.Data 
    • (4)程式上,要加上 private TestContext testContextInstance; 的一段
    • (5)程式上,要加上 Data Source,Connection String 可以參考這篇 Data Driven Testing with Visual Studio 2012 – Coded UI Test ,已經整理好了,改改檔名就可以。
    • 這段的程式是這樣的,其實很單純,但是  Visual Studio 2012 就是不會自己做.


5. 要如何把自己的 Function 跟別的 Interface/Library 隔離?  => Fake 機制
  • 有時候測試的時候會希望跟別的 Interface/Library 切割的比較清楚,譬如拿到輸入資料後不寫入資料庫,只要判斷程式邏輯正確,這時要怎麼切割呢? 在 Visual Studio 2012 時加上了 Fake 的機制,請一定要參考這一篇 VS 2012 的 Unit Test與測試總管 的"建立 Fake 組件"  與這一篇 Isolating Code Under Test with Microsoft Fakes ,Fake機制裡面還可以分為 Stub 與 Shim ,各有各的適用對象。以下小弟舉一個簡單的範例,使用的是Shim給大家參考:我要做的是,不去資料庫檢查帳密,我只要確定帳號密碼不一致就可以,所以我用Shim 隔離我的測試程式與實際的程式。


6. 要如何自動Build 自動單元測試?  => TFS 現在5人內免費!
  • 如果你是 one person team (獵人的磊扎,就是下圖那一位,1個人可以當5個人),那你可以不用考慮這個了 "持續整合/持續集成"這個議題,你自己應該會持續的 build,並且你的開發環境上就是 releasable build...

  • 如果你不是 one person team,一定會遇到版本控制、自動整合Build、自動測試的議題。在這部分,我是使用免費的tfs,現在是5人以內免費,適合small team (M$可能不知道台灣軟體廠商 >5個工程師的專案是很大的專案 哈...)。這一段的內容我是參考這一本書 軟體測試實戰 Visual Studio & Team Foundation Server (推! 雖然是visual studio 2010 ,但是還是很有參考價值,不知道會不會改版..) 書中的 "10-03 自動化組件與測試" 。使用方式大致是這樣:
    • (1) Check in 最新的程式版本到 tfs,Solution 應該也包含測試的專案!
    • (2)建立新的 組建定義,設定一下組建方式等等,譬如指定星期一到五中午12:30 tfs自己build自己測試...
    • (3)每天跑完後回去看結果,因為組建設定中,已經包含執行自動測試,所以在build完成後,tfs也會測試,測試的結果也在報告中,下圖是我前幾天的測試結果,可以看到單元測試錯誤! 



後記
  前前後後約花了約 1個月在Survey、熟悉這些工具的操作,並且在目前開發的系統加入單元測試。在開始加入單元測試後,發現了一些自己的盲點 (function執行的結果與預期不一樣),修改後也增加了程式的 robustness, Unit Test 好物!


參考資料

2013年2月7日 星期四

.Net and SAP Integration: shared function RFC_READ_TABLE

  • 前言:RFC_READ_TABLE 是 SAP 提供的 Read SAP Table 的公用程式,對於需要取得SAP資料的.Net Programmer而言,經常會使用到這個程式,因此適合開發成共用層。這一篇則是將RFC_READ_TABLE 包裝成 WCF 提供服務,SAP端則是以 ECC6 Unicode 版本為目的地。對於本篇的內容,歡迎建議與討論!
  • 使用工具:
    • VS 2010、.Net Framework 4.0
    • SAP NCO 3.0 for .Net Framework 4.0:請自行到SAP網站下載,建議取得最新版,SAP更新的滿快的,舊版曾經發生一些奇怪的問題,安裝好的目錄通常在:C:\Program Files (x86)\SAP 底下,會需要 sapnco.dll 與 sapnco_utils.dll。另外,位元版本也需要注意:
      • Winform: 與專案版本相同,如果專案是64bit,則使用64bit
      • Web application、Web Service、WCF: 都使用32bit
    • 黃昭仁大大的"IRfcTable、DataTable、DataSet 資料轉換模組"(http://vsqa.blogspot.tw/2011/08/irfctabledatatabledataset.html):轉換 NCO 回傳的Table資料結構轉換成.Net Programmer適合使用的 Data Table, DataSet,非常好用。
    • SAP RFC_READ_TABLE:要用的好,必須先對這個function 的用法,詳細可以開 SAP GUI 先試試看。建議在開發前,先在SAP確認好資料都正確,SAP RFC_READ_TABLE 正確,在外部呼叫一定都會正確。
  • WCF 運作方式:WCF主要就是提供 RFC_READ_TABLE去設計,服務的input跟SAP相同,只是我沒有放RowCount,個人認為是不需要,各位看官如果覺得有需要,請再加上去就好。
    • 服務的宣告:
      [OperationContract]
      SAPMessageType SAP_RFC_READ_TABLE_UNICODE(string strDest, string strTable, string[] strOptions, string[] strFields);

      參數說明如下:
      > strDest:SAP目的地
      > strTable: 要查詢的SAP Table
      > strOptions: SAP Options 也就是 Where 的條件,每一行有長度限制是72
      > strFields: SAP Fileds 要查詢SAP Table哪一些欄位
      其中,回傳的資料結構(合約)我自行定義的,因為通常Export會回傳執行的狀態,而 Table 則存放實際的資料,所以把這些合成一種資料結構
      [DataContract]
      public class SAPMessageType
      {
          [DataMember]
          public DataTable SAPTable { get; set; }
          [DataMember]
          public string strMessage { get; set; }
          [DataMember]
          public string strMessageCode { get; set; }
      }
      
    • WCF 執行過程:邏輯上與資料庫連結是一樣的,主要分5區,
      • 第一區 宣告區:主要是宣告會使用到的變數等等,小弟比較老一點,所以會先宣告要用到的變數
      • 第二區 連線區:從連線的class取得連線參數,並開始連線
      • 第三區 傳入參數區:指定SAP Function(RFC_READ_TABLE),傳入Fileds, Options
      • 第四區 回傳參數與處理:取得回傳的SAP 資料,並根據Fields所指定的開始切割
      • 第五區 結束區
        [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
        public SAPMessageType SAP_RFC_READ_TABLE_UNICODE(string strDest, string strTable, string strOptions, string[] strFields)
        {
            //
            //第一區 宣告區: logger 是使用nlog
            //
            logger.Info("SAP_RFC_READ_TABLE START UNICODE Version Start");
            SAPMessageType aData = new SAPMessageType();
            string strMessage = "";
            string strMessageCode = "";

            DataTable SAPDataTable = null;
            RfcDestination destination = null;

            try
            {
                //
                //第二區 連線區,連線內容在 SAPSystemConnect.cs 中取出
                //
                SAPSystemConnect aSapCfg = new SAPSystemConnect();
                RfcConfigParameters parameters = aSapCfg.GetParameters(strDest);

                if (parameters == null)
                {
                    //指定的SAP Client不存在
                    strMessageCode = "E";
                    strMessage = "指定的SAP Client不存在";
                }
                else
                {
                    destination = RfcDestinationManager.GetDestination(parameters);
                    RfcSessionManager.BeginContext(destination);

                    destination.Ping();
                    IRfcFunction function = null;

                    //
                    //第三區 傳入參數區
                    //

                    function = destination.Repository.CreateFunction("RFC_READ_TABLE");
                    function.SetValue("QUERY_TABLE", strTable);

                    //OPTIONS
                    if (strOptions != "")
                    {
                        IRfcTable tableOPTIONS = function["OPTIONS"].GetTable();
                        tableOPTIONS.Append();
                        tableOPTIONS.SetValue(0, strOptions);
                    }

                    IRfcTable tableFIELDS = function["FIELDS"].GetTable();
                    if (strFields.Length != 0)
                    {
                        foreach (string fd in strFields)
                        {
                            tableFIELDS.Append();
                            tableFIELDS.SetValue("FIELDNAME", fd);
                        }
                    }

                    function.Invoke(destination);

                    //
                    //第四區 回傳參數與處理
                    //

                    IRfcTable tableRead = function.GetTable("DATA");
                    DataTable dtRealTable = new DataTable(strTable);
                    ArrayList listFields = new ArrayList();
                    foreach (IRfcStructure row in tableFIELDS)
                    {
                        RFC_Table_Schema aRowSchema = new RFC_Table_Schema();
                        aRowSchema.fdOffset = Convert.ToInt16(row.GetString("OFFSET"));
                        aRowSchema.fdLength = Convert.ToInt16(row.GetString("LENGTH"));
                        aRowSchema.fdName = row.GetString("FIELDNAME");
                        aRowSchema.fdType = row.GetString("Type");
                        listFields.Add(aRowSchema);

                        //準備存放整理後資料的DataTable
                        dtRealTable.Columns.Add(row.GetString("FIELDNAME"), typeof(String));
                    }

                    foreach (IRfcStructure row in tableRead)
                    {
                        DataRow realRow = dtRealTable.NewRow();
                        //根據實際資料切Table內容, WA是為分割前的資料
                        String strWA = row.GetString("WA");

                        //unicode
                        #region Unicode切割方式

                        string anewString;

                        foreach (RFC_Table_Schema aRowSchema in listFields)
                        {
                            anewString = "";
                            //根據長度切割
                            //如果自SAP取得資訊,發現後面都是空白,SAP就會自己Trim字串,資料會與Fields提供的資訊不同
                            if ((aRowSchema.fdOffset + aRowSchema.fdLength) > strWA.Length)
                            {
                                if ((strWA.Length - aRowSchema.fdOffset) > 0)
                                {
                                    anewString = strWA.Substring(aRowSchema.fdOffset, (strWA.Length - aRowSchema.fdOffset));
                                }
                            }
                            else
                            {
                                anewString = strWA.Substring(aRowSchema.fdOffset, aRowSchema.fdLength);
                            }

                            realRow[aRowSchema.fdName] = anewString.Replace("\0", "").Trim();
                        }
                        #endregion

                        dtRealTable.Rows.Add(realRow);

                    }

                    //
                    //第五區 結束區
                    //

                    SAPDataTable = dtRealTable;
                    strMessageCode = "S";
                    strMessage = "成功讀取";

                    RfcSessionManager.EndContext(destination);
                    destination = null;
                }

            }
            catch (Exception ex)
            {
                logger.Error(ex.ToString());

                strMessageCode = "E";
                strMessage = ex.ToString();
                if (destination != null)
                {
                    RfcSessionManager.EndContext(destination);
                    destination = null;
                }
            }

            aData.SAPTable = SAPDataTable;
            aData.strMessage = strMessage;
            aData.strMessageCode = strMessageCode;
            logger.Info("SAP_RFC_READ_TABLE START UNICODE Version END");
            return aData;
        }



  • Service 使用端:呼叫時,先引用WCF,依照這樣呼叫:
  • //使用WCF
    SAPServiceReference.SAPServiceClient aClient = new SAPServiceReference.SAPServiceClient();
    SAPServiceReference.SAPMessageType aClientData = null;
    
    //需要的欄位
    string[] strFds2 = new string[] { "BUKRS", "BUTXT", "LAND1" };
    //Where條件
    string[] strOps = new string[] { "LAND1 <> 'TW'", "AND MANDT = '500' " };
    //取得 T001 公司主檔
    aClientData = aClient.SAP_RFC_READ_TABLE_UNICODE("DEV500", "T001", strOps, strFds2);
    aClient.Close();
    
    
    //因為回傳 Datatable,所以可以直接用
    GridView4.DataSource = aClientData.SAPTable;
    GridView4.DataBind();
    //可以有一個Label說明執行的結果
    lbNewErr.Text = aClientData.strMessageCode + ":" + aClientData.strMessage;
    

如果有需要程式參考的大大,請到這邊:

  • https://dl.dropbox.com/u/3330791/Sharecode/SAPWcfService.rar 
    • 程式目前會無法執行,因為要請自行下載SAP NCO 3.0,並加入 sapnco.dll、sapnco_utils.dll

參考資料:

  • SAP NCO 手冊: http://help.sap.com/saphelp_crm700_ehp02/helpdata/EN/4a/097b0543f4088ce10000000a421937/content.htm 
  • 黃昭仁大大的 Blog: http://vsqa.blogspot.tw/ 
  • How-To Use SAP Nco 3 Connector | .Net 4 | Visual Studio 2010: http://klanguedoc.hubpages.com/hub/How-To-Use-SAP-Nco-3-Net-4-Visual-Studio-2010

#異質資料介接

2012年11月26日 星期一

Develop Google Maps API v3 Map Pages with ASP.NET AJAX


  • 前言:Google Maps API 提供服務以來,由於容易開發、API功能多更新快並且又穩定,只要不踩到一些地雷,就不會收錢,所以一直是地圖開發者喜歡用的工具API。這一篇是要介紹,從資料庫取得點位資料套疊到地圖上。我使用的工具是 ASP.Net 使用 Google Map API v3 ,並且用AJAX,是因為如果用標準的asp.net button,會submit/postback 回 server,如果使用者在submit之前就做了一些地圖操作(放大、移動..),就會因為submit to server 會回到初始畫面,這樣的操作模式是滿令人不悅的,所以才需要AJAX方式來處理使用者對地圖的操作。
  • 網頁功能:取得使用者目前座標,繪製地點與精確度的範圍。並根據使用者點選的圖層類型,帶入對應的點位。
  • 瀏覽器限制:因為有用到HTML5,IE8,9確定不行,用 Chrome Ok, IPhone Safari OK
  • 使用元件:
    • Google Map API V3 :必要
    • Visual Studio 2012:必要
    • JQuery mobile:非必要,放著只為了可以在行動裝置上看。
    • HTML5:必要,為了取得目前座標。所以..那個 IE
  • 網頁主要分成 3 區 
    • (1)地圖區:顯示地圖
    • (2) ASP.Net button 區:負責從資料庫取得資料,並呈現在地圖
    • (3) JS Button區:主要是 javascript button,是操作google map的功能,如"取得目前位置"、"清除地圖" 這類的功能

  • 以下列出比較重要的程式片段,完整的程式請到網頁最下方再去下載。
  • HTML Head區:需要注意的是 style 那一區,因為 Google Map DIV必須指定大小,所以指定在Head裡面。其他就是引用 jquery 與 google map api v3
<head>
    <title>Google Maps API v3 and ASP.Net AJAX</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <style type="text/css">
      html { height: 100% }
      body { height: 100%; margin: 0px; padding: 0px }
      #map_canvas { height: 100% }
    </style>
 <link rel="stylesheet" href="/css/jquery.mobile-1.2.0.min.css" />
    <script src="/js/jquery-1.8.2.min.js"></script>
    <script src="/js/jquery.mobile-1.2.0.min.js"></script>
    <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>

  • 再下來就是控制google maps div 的區域,首先是global的變數
        //Global variables
        var map;            //地圖
        var marker;         //地圖上的點
        var infowindow;     //點上面跳出的視窗
        var overlays = [];  //目前地圖上所有套疊的圖層
        var mapcenter = new google.maps.LatLng(25.228664, 121.750202);  //預設的地圖中心

  • 地圖初始化的function,等下會在body onload呼叫。裡面的詳細API 請參考Google 文件。在初始化完成後,呼叫 取得目前座標的function: getLocation。
        function initialize() {
            var mapOptions = {
                zoom: 6,
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                center: mapcenter,
                mapTypeControl: true,
                mapTypeControlOptions:
                {
                    style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
                    poistion: google.maps.ControlPosition.TOP_RIGHT,
                    mapTypeIds: [google.maps.MapTypeId.ROADMAP,
                    google.maps.MapTypeId.TERRAIN,
                    google.maps.MapTypeId.HYBRID,
                    google.maps.MapTypeId.SATELLITE]
                },
                navigationControl: true,
                navigationControlOptions:
                {
                    style: google.maps.NavigationControlStyle.ZOOM_PAN
                },
                scaleControl: true,
                disableDoubleClickZoom: false,
                streetViewControl: true,
                draggableCursor: 'move'
            };

            infowindow = null;
            infowindow = new google.maps.InfoWindow({
                content: "info window content"
            });

            map = new google.maps.Map(document.getElementById('map_canvas'),
            mapOptions);
            
            //取得使用者目前座標
            getLocation();
        }

  • 取得目前位置化的function:使用HTML5的方式取得座標(點)與精確度(圓圈),並繪製到地圖上。
        //
        //取得目前位置
        //
        function getLocation() {
            var x = document.getElementById("Message");
            if (navigator.geolocation) {
                navigator.geolocation.getCurrentPosition(showPosition);
                //如果要持續取得,用watchPosition
//navigator.geolocation.watchPosition(showPosition); } else { x.innerHTML = "Geolocation is not supported by this browser."; } } function showPosition(position) { var x = document.getElementById("Message"); //x.innerHTML = "Latitude: " + position.coords.latitude + " Longitude: " + position.coords.longitude + " Accuracy" + position.coords.accuracy; var newMapCenter = new google.maps.LatLng(position.coords.latitude, position.coords.longitude); map.setCenter(newMapCenter); //以marker方式出現 setMarker(newMapCenter, "My Current Location" + " Accuracy is :" + position.coords.accuracy); //以圓形出現 if (position.coords.accuracy != null) { DrawCircle(newMapCenter, position.coords.accuracy); if (position.coords.accuracy < 1000) { map.setZoom(14); } } else { DrawCircle(newMapCenter, 100); } }
  • 繪圖用的function,繪製圓形、繪製單一點座標與視窗(infowindow)訊息
        //繪製圓形
        function DrawCircle(center,rad) {
            var draw_circle;
            draw_circle = new google.maps.Circle({
                center: center,
                radius: rad,
                strokeColor: "#FF0000",
                strokeOpacity: 0.8,
                strokeWeight: 2,
                fillColor: "#FFFF00",
                fillOpacity: 0.35,
                map: map
            });
            overlays.push(draw_circle);
        }

        //繪製單一點座標
        function setMarker(singleCoord,infoWindowContent) {
            marker = new google.maps.Marker({
                map: map,
                draggable: true,
                animation: google.maps.Animation.DROP,
                position: singleCoord
            });

            google.maps.event.addListener(marker, 'click', function () {
                infowindow.setContent(infoWindowContent);
                infowindow.open(map, this);
            });

            //
            overlays.push(marker);
        }

        //產生彈跳效果
        function toggleBounce() {
            if (marker.getAnimation() != null) {
                marker.setAnimation(null);
            } else {
                marker.setAnimation(google.maps.Animation.BOUNCE);
            }
        }
  • 繪製多個點,提供給asp.net程式呼叫。Javascript Array的格式是['Mount Evans', 59.32522, 18.17002, 4, 'This is Mount Evans.'];,C#要繪製多點的時候,就是去準備出這個多筆陣列,再交給 setMarkers 去繪製。
        function setMarkers(markers) {
            for (var i = 0; i < markers.length; i++) {
                var sites = markers[i];
                var siteLatLng = new google.maps.LatLng(sites[1], sites[2]);
                var marker = new google.maps.Marker({
                    position: siteLatLng,
                    map: map,
                    title: sites[0],
                    zIndex: sites[3],
                    html: sites[4]
                });

                var contentString = "info window content";

                google.maps.event.addListener(marker, "click", function () {
                    infowindow.setContent(this.html);
                    infowindow.open(map, this);

                    if (marker.getAnimation() != null) {
                        marker.setAnimation(null);
                    } else {
                        marker.setAnimation(google.maps.Animation.BOUNCE);
                    }
                });

                //
                overlays.push(marker);
            }
        }
  • 清除全部繪製的圖層
        function clearMarkers() {
            while (overlays[0]) {
                overlays.pop().setMap(null);
            }
        }
  • 另外在.ASPX網頁設計上,只有ASPX的Controls(Button1~4,顯示A~D類的圖層資料),放在UpdatedPanel裡面。Google Map DIV 更要在Form 以外才能正常顯示。控制地圖的HTML Button(Button 6,7)則是單純的HTML button就可以
<div id="Message"></div>
    <div id="map_canvas" style="width:100%; height:80%"></div>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                AJAX Button:<asp:Button ID="Button1" runat="server" OnClick="Button1_Click" Text="All" />
                <asp:Button ID="Button2" runat="server" OnClick="Button2_Click" Text="A" />
                <asp:Button ID="Button3" runat="server" OnClick="Button3_Click" Text="B" />
                <asp:Button ID="Button4" runat="server" OnClick="Button4_Click" Text="C" />
                <asp:Label ID="Label1" runat="server" Visible="False"></asp:Label>
                <br />
                <asp:Label ID="lbErr" runat="server" Visible="False"></asp:Label>
                <br />
            </ContentTemplate>
        </asp:UpdatePanel>
        JS Button:
        <input id="Button5" type="button" value="Find My Location" onclick="getLocation()"/><input id="Button6" type="button" value="Clear Markers" onclick="clearMarkers()" /></form>
  • C#程式端,以Button3為例,這個按鈕的目的是取得B類的座標並繪製於地圖上
    protected void Button3_Click(object sender, EventArgs e)
    {
        Label1.Text = System.DateTime.Now.ToLongTimeString();
        Label1.Visible = true;

        string strJS = "";
        ArrayList alMarkers = GetMarkers("B");

        strJS = @"var sites = [" + String.Join(",", alMarkers.ToArray()) + "];setMarkers(sites);";
        ScriptManager.RegisterStartupScript(this.Page, this.Page.GetType(), "alert", strJS, true);
    }
  • C#程式端,GetMarkers的目的是從資料庫取出對應的
protected ArrayList GetMarkers(string strType)
    {
        ArrayList alMarkers = new ArrayList();
        try
        {
            string conn = ConfigurationManager.ConnectionStrings["mydevConnectionString"].ConnectionString;
            using (SqlConnection connection = new SqlConnection(conn))
            {
                connection.Open();

                SqlCommand command = connection.CreateCommand();
                command.Connection = connection;

                if (strType == "ALL")
                {
                    command.CommandText = @"SELECT * FROM [MyDev].[dbo].[MyMap] with (nolock)";
                }
                else
                {
                    command.CommandText = @"SELECT * FROM [MyDev].[dbo].[MyMap] with (nolock) where Category=@Category";
                    command.Parameters.Add("@Category", SqlDbType.NVarChar, 20).Value = strType;
                }

                using (SqlDataReader dr = command.ExecuteReader())
                {
                    while (dr.Read())
                    {
                        alMarkers.Add("['" + dr["Project"].ToString() + "', " + dr["Latitude"].ToString() + "," + dr["Longtitude"].ToString() + ", 1, '" + dr["Info"].ToString() + "
" + dr["Addr"].ToString() + "']");
                    }
                }
            }
        }
        catch (Exception ex)
        {
            lbErr.Text = ex.ToString();
            lbErr.Visible = true;
        }
        return alMarkers;
    }

  • Table的設計很簡單,主要就是 Latitude(緯度), Longtitude(經度),其他都是輔助描述的欄位
CREATE TABLE [dbo].[MyMap](
 [ID] [bigint] IDENTITY(1,1) NOT NULL,
 [Category] [nvarchar](20) NOT NULL,
 [Code] [nvarchar](20) NULL,
 [Info] [nvarchar](200) NULL,
 [Project] [nvarchar](50) NULL,
 [Addr] [nvarchar](100) NOT NULL,
 [Latitude] [float] NOT NULL,
 [Longtitude] [float] NOT NULL,
 [Coord] [geography] NULL
) ON [PRIMARY]


完成,結果如圖:
一進入網頁時,會直接定位目前座標,My Current Location 是我目前的座標,黃色是72公尺的精準度,其他的marker是點了B,帶入B類的座標,顯示在地圖上。

歡迎討論!

程式下載:
https://dl.dropbox.com/u/3330791/Sharecode/GoogleMapV3_ASPNET.rar

reference:
https://developers.google.com/maps/documentation/javascript/reference

2012年11月19日 星期一

[Toolkit] Get to Get coordinates in Images ? / 如何批次取得照片中的座標?

  • 目的:手邊有一些照片有座標資訊,現在需要取出座標內容進行後續的加值,除了一張一張照片打開看EXIF座標欄位以外,希望可以批次取得。
  • 使用工具:感謝這位日本網友 nissuk (https://github.com/nissuk),提供可用的powershell (https://gist.github.com/1360596),但是在中文環境下,會有問題,所以小弟有修改過,放在 http://dl.dropbox.com/u/3330791/get-exif.ps1 
  • 使用方式:
1 集合照片到同一個目錄,把 ps 直接放到到照片的目錄下

2 以Powershell執行 ps1,如果不只jpg,則請修改ps1的最後一行:
原本:dir "*.jpg" | get-exif | select name, latitude, longitude | export-csv
改為:dir "*.gif" | get-exif | select name, latitude, longitude | export-csv out.csv

2 執行結果:產生CSV檔案,這樣就完成了! 超級方便!


對這些有興趣的朋友,請看這個PowerShell Image module:
http://archive.msdn.microsoft.com/PSImage/

2012年10月19日 星期五

[Excel] 每個開發人員都需要知道的Excel公式


在這邊介紹幾個小弟經常用到的Excel公式,希望對大家使用Excel上有幫助。

1.字串處理
字串處理是用 Excel 給資訊人員最大的福利,尤其是需要批次處理時,不論是執行SQL、執行powershell 或整理報表,用Excel產生多筆的指令再去執行,真是省下很多作業的時間!

(1)合併Excel的欄位值:Excel的指令是=Concatenate(字串1, 字串2, 字串3...),譬如 =CONCATENATE("insert into tb  ([MyID],[MyDate1],[MyDate2],[MyCompany],[MyPosition],[Description])   values (",A2,",'",TEXT(C2,"yyyy/mm/dd"),"','",TEXT(D2,"yyyy/mm/dd"),"','",E2,"','",J2,"','",H2,"')")
的意思就是從A2~H2取欄位值出來,組合成 insert 指令。寫好第一行後,在Excel依次往下拉,就可以產生很多 insert 指令再貼到下Query的工具上,速度!

(2)取出固定文字的左邊或右邊:這種也常遇到,對於辦公室人員,Excel有些是要看的,但是不一定是結構性的資料,所以會有 "員工編號-XXX" 在同一個欄位出現,該如何處理呢 ? 譬如 1000-甲 要找出"-"左邊的ID,可以用 Find 這個公式找到"-"的位置,搭配Left,公式是 =LEFT(A1, FIND("-",A1)-1) ,長度要 "-1"是因為只要"100"。

如果要找右邊的,公式就寫成 =RIGHT(A1,LEN(A1)-FIND("-",A1))
公式的內容應該是不難懂,主要是組合多個公式起來,如果一開始不確定,也可以先個別計算,這樣就可以一步一步看結果。
(3)補0(或其他需要重複的字):主要是用REPT(重複字) 跟 LEN(值的文字長度)組合使用,譬如100,2000,30000都要補成 5碼的長度,公式是=CONCATENATE(REPT("0",5-LEN(B1)),B1),
分解步驟的話,公式的目的是
 - 重複0多次,要重複的次數:固定長度5 減掉100的長度(=3),所以是重複0共2次
 - 字串合併 00 與 100

2.資料取得對應值:在Excel 處理資料時,會遇到需要取得另一個表的值,也就是資料庫的join,可以使用: vlookup 這個公式,公式的格式是=Vlookup(來源的值,要查的範圍,要回傳的欄位位置,是否要完整比對)。
以下圖為例,公式是=VLOOKUP(A1,D:E,2,FALSE)  "我要找 A1 對應的值[第1個參數],去D與E構成的對應表去找[第2個參數],當A1對應到Excel Column D的值時,回傳第2行[第3個參數],比對時要完全正確才回傳[第4個參數]。
如果查不到,就會出現#N/A的訊息。

而#N/A是不是很刺眼呢,如果又要拿來串SQL指令時,#N/A會造成CONCATENATE失敗。
該如何避免呢?Excel有另一個公式叫做 ISError ,類似寫程式的try catch例外判斷,搭配IF,如: =IF(ISERROR(VLOOKUP(A1,D:E,2,FALSE)),"--",VLOOKUP(A1,D:E,2,FALSE))
意思是 "如果查到的結果是錯誤,就出現 --,如果不是錯誤,就出現查詢的結果",
看不到 #N/A,爽度也提高了

要注意的是
  • Vlookup第2個參數的對應表必須要排序過,要不然是查不到的。
  • 要拿來查詢的值與與對應表的值型態一樣才查的到,1(數字) 與 "1"(文字)是無法比對的上,用改格式的方式去處理會沒用,要用Text這個函數把數字改成文字,1才會變成"1"
  • 如果資料量很大,vlookup 搭配 iserror可能會花時間,不如copy結果出來成另一個Excel在交出去。
另外,
如果你的Excel很複雜,需要查多值,請google "excel array formula vlookup" 希望可以找到你要的答案。
Vlookup也可以自己查自己,因此可以做到類似 recursive 的效果,最適合在產生樹狀結構,如組織的結構,只要把key設為上層,就可以產生出 //公司//董事會//董事長//總經理//台灣分公司//資訊部 這樣的多層結構!

3.多IF的判斷:Excel 最單純的 IF是 =IF(條件,條件成立的值,條件不成立的值)。如果要做類似多層的IF判斷時,也就是要 Switch 時或 nested if ,Excel 也是用IF來達成,公式的範例

=IF(A1="A","1",
  IF(A1="B","2",
  IF(A1="C","3","N")
 ))
這樣Excel就會判斷
Switch(A1)
{
 case "A":"1"
 case "B":"2"
 case "C":"3"
 default: "N"
}
可以看圖也許比較清楚,公式內也可以按 ALT+Enter斷行作些簡單的程式排版,這樣就比較容易知道自己在寫些甚麼了!


Excel還有一些功能,像是清單整理出唯一值(如 SQL Distinct)、Column 轉 Row,都還滿常用的!
歡迎留言交流!

2012年5月12日 星期六

[Sharepoint 2010] 用Powershell建Sharepoint群組並指定權限 / Create Group Powershell and add permission


管理權限一直是各種系統的基礎重點,在Sharepoint跟AD的搭配上,都會以AGDLP去講該如何規劃設計。因此在管理 Sharepoint 群組的權限上,系統管理者(ㄞ  ㄊㄧ  ㄓㄨㄢ  ㄩㄢˊ)又要開熟悉Sharepoint Web/ Sharepoint Designer友善的畫面,以下要說明的是
1.在頂層網站建Group
2.把新建好的Group加到各下層的網站,並停止繼承
3.改下層網站各清單的權限


1.在頂層網站建Group,譬如有3個site
$web = Get-SPWeb "http://sps2010/"
$web.SiteGroups.Add("site1_A", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site1_B", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site1_C", $web.Site.Owner, $web.Site.Owner, "")
$web.SiteGroups.Add("site2_A", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site2_B", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site2_C", $web.Site.Owner, $web.Site.Owner, "")
$web.SiteGroups.Add("site3_A", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site3_B", $web.Site.Owner, $web.Site.Owner,"")
$web.SiteGroups.Add("site3_C", $web.Site.Owner, $web.Site.Owner, "")
$web.Update()
$web.Dispose()

這一段沒什麼學問,如果需要一次建很多,其實可以用EXCEL去組合上面的字。建完Group後,如果需要先放群組的權限,建議用Sharepoint Designer,用Web去管理會在那邊等等等...

2.把新建好的Group加到各下層的網站,並停止繼承
###########################
#
# 函示:add group permission
#
###########################
function AddGroupToSite ($web, $groupName, $permLevel)
{
    $account = $web.SiteGroups[$groupName]
    $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
    $role = $web.RoleDefinitions[$permLevel]
    $assignment.RoleDefinitionBindings.Add($role);
    $web.RoleAssignments.Add($assignment)
}
#########

#Subsites待修改:看有哪些子網站要執行
$SubSites = @("site1","site2","site3"
)
for($i=0 ; $i -lt $SubSites.count ; $i++)
{
 $url = "http://sps2010/" + $SubSites[$i]
 $web = Get-SPWeb $url
 $web.BreakRoleInheritance($false)
 #Subsites待修改,看Group的名字
 $grp1 = $SubSites[$i]+"_A"
 $grp2 = $SubSites[$i]+"_B"
 $grp3 = $SubSites[$i]+"_C"
 AddGroupToSite -web $web -groupName $grp1 -permLevel "Read"
 AddGroupToSite -web $web -groupName $grp2 -permLevel "Read"
 AddGroupToSite -web $web -groupName $grp3 -permLevel "Read"
 $web.Dispose() 
 Write-Output $SubSites[$i] + " Completed!"
}

3.改下層網站各清單的權限
$web = Get-SPWeb "http://sps2010/"
#List Permission
#
#兩個清單要改:
#ListA: url是 /Site1/List/ListA
#ListB: url是 /Site1/SitePicLib
#如果有更多要改,就一直加在函示裡面
#
###########################
#
# 函示:Change List Permission
#
###########################
function ChangeListPermission ($strSiteName)
{
 $SubSites = $strSiteName
 $grp1 = $SubSites+"_A"
 $grp2 = $SubSites+"_B"
 $grp3 = $SubSites+"_C"
 $url = "http://sps2010/" + $SubSites
 $web = Get-SPWeb $url
 $admaccount = $web.EnsureUser("SHAREPOINT\system")
 #############################ListA#############################
 #$ListR = $web.Lists["ListA"]
 $strListURL = "/" + $SubSites + "/Lists/ListA"
 $ListR = $web.GetList($strListURL)
 $ListR.BreakRoleInheritance($false)
 $account = $web.SiteGroups[$grp2]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp4]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $ListR.RoleAssignments.Remove($admaccount)
 #############################ListB#############################
 $strListURL = "/" + $SubSites + "/SitePicLib"
 $ListR = $web.GetList($strListURL)
 $ListR.BreakRoleInheritance($false)
 $account = $web.SiteGroups[$grp2]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp4]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add(($web.RoleDefinitions | Where-Object { $_.Type -eq "Contributor" }))
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp1]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add($web.RoleDefinitions["Read"])
 $ListR.RoleAssignments.Add($assignment)
 $account = $web.SiteGroups[$grp3]
 $assignment = New-Object Microsoft.SharePoint.SPRoleAssignment($account)
 $assignment.RoleDefinitionBindings.Add($web.RoleDefinitions["Read"])
 $ListR.RoleAssignments.Add($assignment)
 $ListR.RoleAssignments.Remove($admaccount)
 $web.Dispose()
}
###########################

#待修改
#實際呼叫函示
ChangeListPermission -strSiteName "site1"
ChangeListPermission -strSiteName "site2"
ChangeListPermission -strSiteName "site3"

權限這樣就差不多設定完成了,搭配Excel更快!
如果要對Sharepoint Group加AD Group,請看下一篇!


參考資料:
1.PowerShell to create SharePoint groups http://blog.pointbeyond.com/2011/06/03/powershell-to-create-sharepoint-groups/


#DontLikeSP

2012年3月1日 星期四

[Sharepoint 2010] 用Powershell 建網站 / Create web with Powershell

有鑑於 Sharepoint建網站的UI太友善了,讓要建多個網站的管理員會非常熟悉建站的動作跟設定,因此小弟到蒐集網路上各種建站的Powershell,東拼西湊成一個建多個站的Powershell:

1.如果有自訂的web template,則需要找出ID跟Name,可用:

 #待修改
 $url = "http://sps2010/"
 $site= new-Object Microsoft.SharePoint.SPSite($url )
 $loc= [System.Int32]::Parse(1033)
 $templates= $site.GetWebTemplates($loc)
   foreach ($child in $templates)
      {
               write-host $child.Name "  " $child.Title
      }
 $site.Dispose()





查詢出的結果會類似
{2F1B367A-5FF5-444E-B3BC-DBB73E1FEDXX}#SiteTemplateName    SiteTemplate_Title
只需要用到前面的,Title只是用來識別的

2.create it!
 以下的Powershell有包含以下步驟
 (1).建立一個新的網站
 (2).設定 "網站設定"的"其他語言",增加中文(1028)
 (3).設定 "網站設定"的"覆寫翻譯"
 (4).設定 "管理網站功能"的 "SharePoint Server Publishing"(為了master page)
 (5).設定 Master Page


#待修改

$SiteCollectionURL = "http://sps2010"

#待修改


$SiteCollectionTemplate = "
{2F1B367A-5FF5-444E-B3BC-DBB73E1FEDXX}#SiteTemplateName "

#預設語系英文

$SiteCollectionLanguage = 1033

#新站的URL

$SubSites = @("UrlA",

"UrlA",

"UrlA",

)

#新站的標題(Title)

$SubSiteNames =@("TitleA",

"TitleB",

"TitleC",

)

for($i=0 ; $i -lt $SubSites.count ; $i++)

{

 #(1).建立一個新的網站,根據指定的範本與語系,描述則預設為空白

 $SiteUrl = ""

 $SiteUrl = $SiteCollectionURL + "/"

 $SiteUrl = $SiteUrl += $SubSites[$i]

 $web=New-SPWeb $SiteUrl -Name $SubSiteNames[$i]  -UseParentTopNav -Language $SiteCollectionLanguage -Description " "
 #在我的環境,要再套用一次才會work, 不知道為什麼!
 $web.ApplyWebTemplate($SiteCollectionTemplate) 

 #(2).設定 "網站設定"的"其他語言",增加中文(1028)

 $web.IsMultilingual = $true

 $spReg = New-Object Microsoft.SharePoint.SPRegionalSettings $web

 #顯示安裝的language pack

 $spReg.InstalledLanguages

 $web.AddSupportedUICulture(1028)

 #(3).設定 "網站設定"的"覆寫翻譯"

 $web.OverwriteTranslationsOnChange = $true;

 #(4).設定 "管理網站功能"的 "SharePoint Server Publishing"(為了master page)

 Enable-SPFeature -identity "PublishingWeb" -URL $SiteUrl

 $web.Dispose()

 #Write-Output "完成: " += $SubSites[$i]


}

#

#(5).設定 Master Page

#因為需要web.update 所以分開執行

#

for($i=0 ; $i -lt $SubSites.count ; $i++)

{

 $SiteUrl = ""

 $SiteUrl = $SiteCollectionURL + "/"

 $SiteUrl = $SiteUrl += $SubSites[$i]

 $web = Get-SPWeb $SiteUrl

 $web.AllProperties["__InheritsCustomMasterUrl"] = "False";

 #待修改

 $web.CustomMasterUrl = "/_catalogs/masterpage/myv4.master"

 $web.AllProperties["__InheritsMasterUrl"] = "False";

  #待修改

 $web.MasterUrl = "/_catalogs/masterpage/myv4.master"

 $web.Update()

 $web.Dispose()

}


希望sharepoint後代多考慮管理員批次處理的功能,時間不是用來做重複的管理工作!

參考:



#DontLikeSP

2012年2月18日 星期六

[How to] 如何用C#使用Windows Fax / Fax with C# in Windows Fax Service Environment

[How to] Fax with C# in Windows Fax Service Environment
透過 Windows Fax Service,以程式方式發送傳真
  • 使用語言: C#
  • 加入參考 ( FXSRESM.dll ) 路徑在: C:\windows\system32\FXSRESM.dll
  • Code 如下:
    FaxServer faxServer = new FaxServer();
    //IP是Fax Server IP
    faxServer.Connect("192.168.1.1");
    FaxDocument aDoc = new FaxDocument();
    //要FAx的檔案
    aDoc.Body = "C:\\Temp\\1.docx";
    aDoc.ReceiptAddress = "stace@xxx";
    //收傳真的電話
    aDoc.Recipients.Add("88880000", "Recp");
    aDoc.ConnectedSubmit(faxServer);

  • 就這樣就可以發Fax出去, Stupid and simple! Good!
  • 其他傳真的封面/封底/Server傳送的狀態還需要再詳細看SDK與 COM Object的其他method! 基本上應該還要確認一下對方是否有傳成功,就類似人為動作一樣..

參考:

  • Using the Fax Service SDK http://msdn.microsoft.com/en-us/library/windows/desktop/ms693392(v=vs.85).aspx
  • Fax Service Extended COM Objects http://msdn.microsoft.com/en-us/library/windows/desktop/ms693456(v=vs.85).aspx