例大祭5 マッピング支援ツール wakimap5 v0.041
例大祭5 マッピング支援ツール wakimap5 v0.04
version up
2008/05/22 v0.04
「名前を付けて保存」「読み込み」を行った場合サークルのURIが設定されていない場合のページが正しく表示できなかったバグを修正。
ブラウザウインドウで使用しているブラウザコントロールがIEの問題で不正な処理を行った場合、エラーを起こして終了してしまうため、問題のあるURLをフィルタリングするように変更。
リストに変更が加えられた場合、30秒間隔で自動的にリストを保存する機能を追加。実行ファイルのあるディレクトリのautosave.datに保存されます。
例大祭5 マッピング支援ツール wakimap5 v0.03
version up
2008/05/21 v0.03
メニュー、ステータスバーを追加。
左右リスト描画オプションを追加。
リストの描画スタイルについて
v0.03から「リストを左右に分けて描画」オプションが実装されました。
このオプションが有効になっている場合、リストが左右に描画されます。
会場の左側にあるサークルは左側のリストに、右側のサークルは右側のリストにそれぞれ描画されるようになります。
チェックを外すと、従来通りの右側1列レイアウトで描画します。
例大祭5 マッピング支援ツール wakimap5 v0.02
2008/05/17 v0.02
検索ウインドウ、ブラウザウインドウを追加。
メインウインドウのリサイズに対応。
つかいかた
右上のリストボックスには全サークルのリストが、右下のリストボックスには選択したサークルのリストがそれぞれ表示されています。
2つのリストボックスの間には、選択されているサークルのサークル名やペンネーム、WebサイトのURIが表示されます。
配置図上の任意のスペースをクリックすると、対応するサークルが全サークルリストで選択されます。
サークルが選択されている状態で、キーボードの1-9を押すと、対応する色でそのサークルを塗ります。0を押すと色指定を解除します。
同様の操作を、右クリックメニューで行うこともできます。
配置図上では、右クリックを押しっぱなしにしたままマウスをドラッグすることでスクロールすることができます。
[新規]ボタンをクリックするとリストをクリアします。
[読込]ボタンをクリックすると、リストファイルから選択サークル情報を読み込みます。
[保存]ボタンをクリックすると、現在の選択サークル情報をリストに保存します。
[上書]ボタンをクリックすると、上書き保存が可能な場合には上書き保存を行います。
[画像保存]ボタンをクリックすると、現在表示されている配置図画像(選択中のサークルを示す赤枠と紫の座標ガイドを除く)をPNG形式で保存します。
サークル情報部にある[検]ボタンをクリックすると、そのサークルのサークル名とペンネームをgoogleで検索します。
[開]ボタンをクリックすると、WebページのURIがある場合にはブラウザでそのURIを開きます。
なお、本バージョンでは印刷機能をサポートしていません。ペイントや任意のビューアなどを使って印刷して下さい。
別ウインドウ操作について
v0.02から「検索ウインドウ」と「ブラウザウインドウ」が追加されました。
検索ウインドウ
ウインドウ下部にあるテキストボックスに検索したい文字列を入力し、検索ボタンをクリックすると、その文字列を「サークル名」「ペンネーム」に持つサークルを検索します。
また、テキストボックスはオートコンプリートが有効になっているので先頭から一致入力候補が表示されるようになっています。
検索結果として表示されたリストをクリックすると、メインウインドウのリストに反映されます。
既知のバグ
キーボードで連続して色を指定した場合、その直後にカーソルキーで移動しようとすると色指定をはじめたアイテムの位置からの移動となる。
選択サークルの数が増えると、配置図からリストがはみ出す。
サークルにURIが設定されていない場合のページで、"I'm Feeling Lucky"をクリックしても検索結果が表示されることがある。(googleの仕様?)
その他諸々。
例大祭5 マッピング支援ツール wakimap5 v0.01pre
つかいかた
右上のリストボックスには全サークルのリストが、右下のリストボックスには選択したサークルのリストがそれぞれ表示されています。
2つのリストボックスの間には、選択されているサークルのサークル名やペンネーム、WebサイトのURIが表示されます。
配置図上の任意のスペースをクリックすると、対応するサークルが全サークルリストで選択されます。
サークルが選択されている状態で、キーボードの1-9を押すと、対応する色でそのサークルを塗ります。0を押すと色指定を解除します。
同様の操作を、右クリックメニューで行うこともできます。
配置図上では、右クリックを押しっぱなしにしたままマウスをドラッグすることでスクロールすることができます。
[新規]ボタンをクリックするとリストをクリアします。
[読込]ボタンをクリックすると、リストファイルから選択サークル情報を読み込みます。
[保存]ボタンをクリックすると、現在の選択サークル情報をリストに保存します。
[上書]ボタンをクリックすると、上書き保存が可能な場合には上書き保存を行います。
[画像保存]ボタンをクリックすると、現在表示されている配置図画像(選択中のサークルを示す赤枠と紫の座標ガイドを除く)をPNG形式で保存します。
サークル情報部にある[検]ボタンをクリックすると、そのサークルのサークル名とペンネームをgoogleで検索します。
[開]ボタンをクリックすると、WebページのURIがある場合にはブラウザでそのURIを開きます。
なお、本バージョンでは印刷機能をサポートしていません。ペイントや任意のビューアなどを使って印刷して下さい。
既知のバグ
キーボードで連続して色を指定した場合、その直後にカーソルキーで移動しようとすると色指定をはじめたアイテムの位置からの移動となる。
選択サークルの数が増えると、配置図からリストがはみ出す。
その他諸々。
C#で、ニコニコ動画のコメントXMLデータを取得する
ツールを使ったニコニコ動画へのアクセスは推奨されていません。
下記のコード・ツールを使った事により生じるあらゆる責任を
筆者は負えませんのであらかじめご了承下さい。
ニコニコ動画処理クラス
ログイン・動画/コメント情報の取得などを行う niconico クラスと、コメント情報を格納する nicoComment クラス
using System; using System.Collections; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Net; using System.IO; using System.Xml; namespace nicoCommentGetter { class niconico { // //////////////////////////////////////////////////////////////////////////////////////// // private members // //////////////////////////////////////////////////////////////////////////////////////// // uri of api to logging in private const string URI_Login = "https://secure.nicovideo.jp/secure/login?site=niconico"; // cookie container for login-cookie private CookieContainer cookie_login = null; // regex pattern for smilevideo uri private const string REGEX_PTN_sm_uri = @"^http://www.nicovideo.jp/watch/sm(?<videoid>[0-9]+)$"; // getflv uri private const string URI_getflv = "http://www.nicovideo.jp/api/getflv?v=sm"; // //////////////////////////////////////////////////////////////////////////////////////// // public members/properties // //////////////////////////////////////////////////////////////////////////////////////// // returns true is this instance is logged in. public bool isLogin { get { return this._isLogin; } } private bool _isLogin = false; public niconico(string username, string password) { // login bool login_result = login(username, password); } /// <summary> /// login into nicovideo /// </summary> /// <param name="username">username (usual mailaddress)</param> /// <param name="password">password</param> /// <returns>login succeeded or not</returns> public bool login(string username, string password) { // hashtable to hold the arguments of POST request. Hashtable post_arg = new Hashtable(3); post_arg["mail"] = username; post_arg["password"] = password; post_arg["next_url"] = ""; // create cookie-container this.cookie_login = new CookieContainer(); // send POST request string ret = HttpPost(URI_Login, post_arg, ref this.cookie_login); // check if result contains "ログインエラー" if (ret.IndexOf("ログインエラー") != -1) { this.cookie_login = null; this._isLogin = false; return false; } this._isLogin = true; return true; } public Hashtable getMovieInfo(string URI) { // video id string vid = ""; // check already logged in or not if (this._isLogin == false) return null; // check have a valid cookie or not if (this.cookie_login == null) return null; // check url Regex regex_uri = new Regex(REGEX_PTN_sm_uri); Match regex_match = regex_uri.Match(URI); if (regex_match.Success) { vid = regex_match.Groups["videoid"].ToString(); } else { return null; } // send request (GET) string getflv_uri = URI_getflv + vid; string response = HttpGet(getflv_uri, ref this.cookie_login); // parse the result // hashtable to hold the values Hashtable ret = new Hashtable(); // urldecode string dec = System.Web.HttpUtility.UrlDecode(response, Encoding.UTF8); // parse by "&" string[] pairs = dec.Split(new char[] { '&' }); // store key-value into hashtable foreach (string item in pairs) { // parse by '=' string[] keyvalue = item.Split(new char[] { '=' }); if (keyvalue.Length != 2) continue; // store each entry ret[keyvalue[0]] = keyvalue[1]; } return ret; } public XmlTextReader getCommentXML(string URI, int commentCount) { Hashtable minfo = getMovieInfo(URI); if (minfo == null) return null; // uri of message server string uri_message = minfo["ms"] as string; if (uri_message == null) return null; // thread id string tid = minfo["thread_id"] as string; if (tid == null) return null; // request argument string req = string.Format( "<thread res_from=\"-{0}\" version=\"20061206\" thread=\"{1}\" />", commentCount, tid ); // send request string response = HttpPost(uri_message, req, ref this.cookie_login); // recreate stream (for xml stream) // ref: http://namespacetest.blogspot.com/2007/06/xmltextreaderstringxml.html MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(response), false); // create XML data XmlTextReader reader = new XmlTextReader(ms); return reader; } // //////////////////////////////////////////////////////////////////////////////////////// // common functions // // get/post request ref: // http://www.atmarkit.co.jp/fdotnet/dotnettips/326cookie/cookie.html /// <summary> /// Get data using GET request /// </summary> /// <param name="url">URI to send request</param> /// <param name="cc">CookieContainer to hold the cookie data</param> /// <returns>response data (string)</returns> private string HttpGet(string url, ref CookieContainer cc) { // Create HttpWebRequest HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.CookieContainer = cc; WebResponse res = req.GetResponse(); // read response Stream resStream = res.GetResponseStream(); StreamReader sr = new StreamReader(resStream, Encoding.UTF8); string result = sr.ReadToEnd(); sr.Close(); resStream.Close(); return result; } /// <summary> /// Get data using POST request (Hash version) /// </summary> /// <param name="url">URI to send request</param> /// <param name="vals">POST parameter</param> /// <param name="cc">CookieContainer to hold the cookie data</param> /// <returns>response data (string)</returns> private string HttpPost(string url, Hashtable vals, ref CookieContainer cc) { // concatinate all key-value pair string param = ""; foreach (string k in vals.Keys) { param += String.Format("{0}={1}&", k, vals[k]); } byte[] data = Encoding.ASCII.GetBytes(param); // create HttpWebRequest HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "POST"; req.ContentType = "application/x-www-form-urlencoded"; req.ContentLength = data.Length; req.CookieContainer = cc; // write POST data Stream reqStream = req.GetRequestStream(); reqStream.Write(data, 0, data.Length); reqStream.Close(); WebResponse res = req.GetResponse(); // read response Stream resStream = res.GetResponseStream(); StreamReader sr = new StreamReader(resStream, Encoding.UTF8); string result = sr.ReadToEnd(); sr.Close(); resStream.Close(); return result; } /// <summary> /// Get data using POST request (Plain Text Version) /// </summary> /// <param name="url">URI to send request</param> /// <param name="arg">POST parameter(plain text)</param> /// <param name="cc">CookieContainer to hold the cookie data</param> /// <returns>response data (string)</returns> private string HttpPost(string url, string arg, ref CookieContainer cc) { byte[] data = Encoding.ASCII.GetBytes(arg); // create HttpWebRequest HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "POST"; req.ContentType = "application/x-www-form-urlencoded"; req.ContentLength = data.Length; req.CookieContainer = cc; // write POST data Stream reqStream = req.GetRequestStream(); reqStream.Write(data, 0, data.Length); reqStream.Close(); WebResponse res = req.GetResponse(); // read response Stream resStream = res.GetResponseStream(); StreamReader sr = new StreamReader(resStream, Encoding.UTF8); string result = sr.ReadToEnd(); sr.Close(); resStream.Close(); return result; } } public class nicoComment { private string _comment = ""; public string comment { get { return this._comment; } } private bool _anonymity = false; public bool anonymity { get { return this._anonymity; } } private string _date = ""; public string date { get { return this._date; } } private string _mail = ""; public string mail { get { return this._mail; } } private int _no = 0; public int no { get { return this._no; } } private string _thread = ""; public string thread { get { return this._thread; } } private string _user_id = ""; public string user_id { get { return this._user_id; } } private int _vpos = 0; public int vpos { get { return this._vpos; } } private bool _premium = false; public bool premium { get { return this._premium; } } /// <summary> /// constructor /// </summary> /// <param name="xmlnode">xml node instance holding comment info</param> public nicoComment(XmlNode xmlnode) { _comment = xmlnode.InnerText; _date = xmlnode.Attributes["date"].Value; _no = Convert.ToInt32(xmlnode.Attributes["no"].Value); _thread = xmlnode.Attributes["thread"].Value; _user_id = xmlnode.Attributes["user_id"].Value; _vpos = Convert.ToInt32(xmlnode.Attributes["vpos"].Value); _mail = ""; if (xmlnode.Attributes["mail"] != null) _mail = xmlnode.Attributes["mail"].Value; _premium = false; if (xmlnode.Attributes["premium"] != null) _premium = (xmlnode.Attributes["premium"].Value == "1"); _anonymity = false; if( xmlnode.Attributes["anonymity"] != null) _anonymity = (xmlnode.Attributes["anonymity"].Value == "1"); } // comparer /// <summary> /// comparer by user-id /// </summary> public class comment_uid_comparer : IComparer<nicoComment> { #region IComparer<nicoComment> メンバ public int Compare(nicoComment x, nicoComment y) { string x1 = x.user_id; string x2 = y.user_id; return string.Compare(x1, x2); } #endregion } /// <summary> /// comparer by vpos /// </summary> public class comment_vpos_comparer : IComparer<nicoComment> { #region IComparer<nicoComment> メンバ public int Compare(nicoComment x, nicoComment y) { int x1 = x.vpos; int x2 = y.vpos; return x1 - x2; } #endregion } /// <summary> /// comparer by comment number /// </summary> public class comment_number_comparer : IComparer<nicoComment> { #region IComparer<nicoComment> メンバ public int Compare(nicoComment x, nicoComment y) { //throw new Exception("The method or operation is not implemented."); int x1 = x.no; int x2 = y.no; return x1 - x2; } #endregion } } }
メイン処理
メインのフォーム処理。GUIの配置などは省略。アーカイブの方を参照して下さい。
コントロールのプレフィクスは、lbl=ラベル、txt=テキストエディット、trv=ツリービュー、nud=ニューメリカルアップダウンです。
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Xml; namespace nicoCommentGetter { public partial class Form1 : Form { // niconico class private niconico nico = null; public Form1() { InitializeComponent(); } private void btnDoExit_Click(object sender, EventArgs e) { this.Close(); } private void btnDoGet_Click(object sender, EventArgs e) { // clear tree this.trvTree.Nodes.Clear(); // comment list List<nicoComment> comList = new List<nicoComment>(); // comparer nicoComment.comment_uid_comparer comp_uid = new nicoComment.comment_uid_comparer(); nicoComment.comment_vpos_comparer comp_vpos = new nicoComment.comment_vpos_comparer(); // login this.nico = new niconico(this.txtLoginName.Text, this.txtPassword.Text); // get comment data XmlTextReader reader = this.nico.getCommentXML(this.txtMovieURI.Text, (int)this.nudCommentCount.Value); // comment edit if (reader != null) { // set cursor to wait-cursor this.Cursor = Cursors.WaitCursor; XmlDocument doc = new XmlDocument(); doc.Load(reader); // fetch all <chat> elements foreach (XmlNode node in doc.GetElementsByTagName("chat")) { // create nicoComment instance and store into list comList.Add(new nicoComment(node)); } // sort by user-id comList.Sort(comp_uid); // last uid (each-user-loop) string lastuid = ""; // subset list List<nicoComment> sublist = null; foreach (nicoComment com in comList) { string uid = com.user_id; TreeNode user_node = null; // check if this comment is a part of same user's comment if (uid == lastuid) { sublist.Add(com); } else { // an user's comments are finished. if (sublist != null) { // begin update this.trvTree.BeginUpdate(); // sort sublist by vpos. sublist.Sort(comp_vpos); // user's children nodes array TreeNode[] node_comment = new TreeNode[sublist.Count]; // store each comment into array (for node) for (int i = 0; i < sublist.Count; i++) { node_comment[i] = new TreeNode(sublist[i].comment); } // user's root node (child of root) user_node = new TreeNode(sublist[0].user_id, node_comment); // add user node this.trvTree.Nodes.Add(user_node); // end update this.trvTree.EndUpdate(); } // discard sublist and create new one. sublist = new List<nicoComment>(); sublist.Add(com); lastuid = uid; } } // set cursor to default cursor this.Cursor = Cursors.Default; // done. if (this.trvTree != null) { this.trvTree.ExpandAll(); this.trvTree.SelectedNode = this.trvTree.Nodes[0]; } } } private void btnDoCopy_Click(object sender, EventArgs e) { // selected node TreeNode clk_topnode = this.trvTree.SelectedNode; if (clk_topnode == null) return; // copy text string str_cpy = ""; // if node has no child if (clk_topnode.GetNodeCount(true) == 0) { str_cpy = clk_topnode.Text; } else { // this node has children. // ignore this node and concatinate children's text. // concatinate using StringBuilder System.Text.StringBuilder sb = new StringBuilder(); foreach (TreeNode tn in clk_topnode.Nodes) { if (tn.Text == string.Empty) continue; sb.Append(tn.Text); sb.Append("\n"); } str_cpy = sb.ToString(); } // send to clipboard Clipboard.SetText(str_cpy); // show message MessageBox.Show(this, "copied: \r\n" + str_cpy, "Comment copy"); } } }
処理の流れ
- ニコニコ動画にログイン、ログインクッキーを取得
- 動画URLから動画ID(sm[0-9]+の数字部分)を取得
- 動画IDを使って getflv APIを呼び出し、動画情報を取得
- 動画情報に含まれるスレッドID、メッセージサーバのURIを使い、メッセージサーバにコメント情報を要求する。
- 取得したコメント情報(XML形式)をXmlDocumentに起こす
- XmlDocumentからchatタグのノードだけを処理する
- 各コメントについて、nicoCommentクラスを作成し、List<nicoComment>に格納する
- 全てのコメントを格納したら、user_idについてソートする
- そのリストを順に走査し、同一user_idのコメントを別のList<nicoComment>に格納する
- 同一user_idのコメントを格納したリストを、vposについてソートする
- ユーザとそのユーザが書き込んだコメントを、ツリービューに登録する
- ツリーのアイテムを選択した状態で"copy comment"がクリックされると、その子ノードの情報をクリップボードにコピーする
あまり例外処理とかしてないので、ユーザ情報がアレだったり動画がアレだったりするとアレかもしれません。
ファイル
ソース入りアーカイブはこちらから。
nicoCommentGetter_v0.1a.zip