using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; using System.Net; namespace KabuPlusDownloader { /* * 処理目的 * 1時間あたりの制限アクセス数(デフォルトとして4を設定)を超えない範囲で * KABU+株価の最新配信CSV、過去配信CSVファイルをダウンロード、パソコン内に保存する * 優先順位通りに都度チェックして不足してるファイルをダウンロードする * * 処理優先順位 * 保存フォルダ内最新日付>次の営業日>その次の営業日>・・・>KABU+が配信した最新日付 * 保存フォルダ内最古日付>前の営業日>その前の営業日>・・・>2018/1/4(KABU+配信初日) * * KABU+配信、一番古い日付 * 上記では2018/1/4からとしたが、実際は2018/1/4 * 株価一覧表(新フォーマット)japan-all-stock-prices-2が2018/1/4からで、これに合わせた * 株価一覧表japan-all-stock-prices、株価一覧表(ETF/REIT/ファンド)に関しては、2017/1/4が最初 * */ public partial class Form1 : Form { /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// private const string SITE_ID = "id"; //要変更 KABU+から支給されるID private const string SITE_PASSWORD = "pass"; //要変更 KABU+から支給されるパスワード private const string HOZON_DIR = @"c:\temp"; //要変更 KABU+からダウンロードするCSV群を保存するフォルダ private const int MAX_DOWNLOAD_NUMBER = 4; //要変更 KABU+契約コースによって違う /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// private const int MIN_DATE = 20180104;//データがこれ以前は存在しない。冒頭の説明参照のこと。 private const int FOR_FUTURE = 1;//プラス1日の意味 private const int FOR_PAST = -1;//マイナス1日の意味 private readonly string[] CSVFILENAME = new string[] { "tosho-etf-stock-prices", "tosho-reit-stock-prices", "tosho-fund-and-others-stock-prices", "japan-all-stock-prices-2" };/*, "japan-all-stock-prices", "japan-all-stock-data" };*/ private string HISTORY_DATE_FILE = string.Format(@"{0}\KabuPlusDownloader.ini", System.Windows.Forms.Application.StartupPath);//最終処理日付時分秒を保存する自動生成ファイル名 public Form1() { //InitializeComponent(); var update_ymdhms = ReadIniFile(); if (!ChkLastDownload(update_ymdhms)) System.Environment.Exit(0);//1時間以内に過去起動していたら強制終了 var min = int.MaxValue; var max = int.MinValue; SearchOldfileNewfile(ref min, ref max);//保存フォルダ内ファイルのファイル名から最新日付と最古日付を取得 Run(min, max, ref update_ymdhms); WriteIniFile(update_ymdhms);//最終当処理時間をファイル保存 System.Environment.Exit(0); } private void Run(int olddate, int newdate, ref string updatetime) { var cnt = 0;//MAXはMAX_DOWNLOAD_NUMBERのカウンター InitialAct(ref olddate, ref newdate);//初回起動対応 DownloadKabuPlusFile(newdate, ref cnt, ref updatetime);//保存フォルダ内最新日付ファイルが全部そろってるか var date_pos = NextDate(FOR_FUTURE, Int2Datetime(newdate)); if (CompareDatetime2Sysdate(date_pos))//未来方向処理 DownloadKabuPlusFile(Datetime2Int(date_pos), ref cnt, ref updatetime); DownloadKabuPlusFile(olddate, ref cnt, ref updatetime);//保存フォルダ内最古日付ファイルが全部そろってるか date_pos = NextDate(FOR_PAST, Int2Datetime(olddate));//1営業日過去方向へ if (CompareDatetime2OpeningDate(date_pos))//過去方向処理 DownloadKabuPlusFile(Datetime2Int(date_pos), ref cnt, ref updatetime); } private bool ChkLastDownload(string ymdhms) { if (ymdhms.Length != 14) return false; var ymd = 0; var hms = 0; if (!int.TryParse(ymdhms.Substring(0, 8), out ymd)) return false; if (!int.TryParse(ymdhms.Substring(8, 6), out hms)) return false; var dt = DateTime.Parse(string.Format("{0}/{1}/{2} {3}:{4}:{5}", ymd / 10000, ymd % 10000 / 100, ymd % 100, hms / 10000, hms % 10000 / 100, hms % 100)); var ts = new TimeSpan(0, 0, 55, 0); if (dt < DateTime.Now - ts)//引数の時間がシステム時間から1時間(コード上は55分とした)以内だったらfalse return true; else return false; } private void SearchOldfileNewfile(ref int min, ref int max) { var files = System.IO.Directory.GetFiles(HOZON_DIR, "*.csv", SearchOption.TopDirectoryOnly); for (int i = 0; i < files.Length; i++) { files[i] = Path.GetFileName(files[i]).Replace(".csv", ""); for (int j = 0; j < CSVFILENAME.Length; j++) files[i] = files[i].Replace(string.Format("{0}_", CSVFILENAME[j]), string.Empty); } Array.Sort(files); for (int i = 0; i < files.Length; i++) { var d = 0; if (int.TryParse(files[i], out d)) { if (min > d) min = d; if (max < d) max = d; } } } private string ReadIniFile() { var ret = string.Empty; if (File.Exists(HISTORY_DATE_FILE)) { using (var sr = new StreamReader(HISTORY_DATE_FILE, System.Text.Encoding.GetEncoding("shift_jis"))) { ret = sr.ReadToEnd(); } } else { //存在しなかったら初回起動とみなす ret = "19000101000000"; } return ret; } private void WriteIniFile(string s) { using (var sw = new StreamWriter(HISTORY_DATE_FILE, false, System.Text.Encoding.GetEncoding("shift_jis"))) { sw.Write(s); } } private void InitialAct(ref int olddate, ref int newdate) { if (newdate < 0) { var date_pos = RetMarketOpenDate(DateTime.Now, FOR_PAST); newdate = Datetime2Int(date_pos); olddate = newdate; } } private DateTime NextDate(int Vektor, DateTime dt) { return RetMarketOpenDate(dt, Vektor); } private DateTime RetMarketOpenDate(DateTime dt, int Vektor) { while (true) { dt = dt.AddDays(Vektor);//Vektorは未来(+1)か過去(-1)か if (IsMarketOpenDate(dt)) break; if (dt < Int2Datetime(MIN_DATE)) break; } return dt; } private bool CompareDatetime2Sysdate(DateTime dt) { if (Datetime2Int(dt) < Datetime2Int(DateTime.Now))//日時(ymd)が過去だったら迷わずTrue return true; else if (Datetime2Int(dt) > Datetime2Int(DateTime.Now))//日時(ymd)が未来だったら迷わずFalse return false; else if (Datetime2Int(dt) == Datetime2Int(DateTime.Now) && int.Parse(DateTime.Now.ToString("HHmm")) > 1610)//日時(ymd)が同じで夕方16時すぎならTrue return true; else return false; } private bool CompareDatetime2OpeningDate(DateTime dt) { if (int.Parse(dt.ToString("yyyyMMdd")) >= MIN_DATE) //dtが2018/01/4以内かどうか return true; else return false; } private void DownloadKabuPlusFile(int date, ref int cnt, ref string updatetime) { for (int i = 0; i < CSVFILENAME.Length; i++) { if (cnt >= MAX_DOWNLOAD_NUMBER) break; var filename = MakeFilename(date, i); var path = string.Format(@"{0}\{1}", HOZON_DIR, filename); if (!File.Exists(path)) { if (Download(date, MakeUrl(date, i), path)) { cnt++; updatetime = DateTime.Now.ToString("yyyyMMddHHmmss"); } else { Console.WriteLine("ダウンロードが何らかの理由で失敗"); } } } } private string MakeUrl(int date, int kind) { var url_base = "https://csvex.com/kabu.plus/csv"; return string.Format("{0}/{1}/daily/{1}_{2:D8}.csv", url_base, CSVFILENAME[kind], date); } private string MakeFilename(int date, int kind) { return string.Format("{0}_{1:D8}.csv", CSVFILENAME[kind], date); } private bool Download(int date, string url, string filename) { var ret = true; var uri = new Uri(url); try { using (var wc = new System.Net.WebClient()) { var namePassword = string.Format("{0}:{1}", SITE_ID, SITE_PASSWORD); var chars = System.Text.Encoding.ASCII.GetBytes(namePassword); var base64 = Convert.ToBase64String(chars); wc.Headers[HttpRequestHeader.Authorization] = "Basic " + base64; //webClient.Credentials = new NetworkCredential(SITE_ID, SITE_PASSWORD); using (var reader = new StreamReader(wc.OpenRead(url), System.Text.Encoding.GetEncoding("shift_jis"))) { var line = reader.ReadToEnd(); if (line.Trim() != string.Empty) { System.IO.File.WriteAllText(filename, line, System.Text.Encoding.GetEncoding("shift_jis")); } } } } catch (Exception ex) { Console.WriteLine(ex.Message); ret = false; } return ret; } private DateTime Int2Datetime(int d) { return DateTime.Parse(string.Format("{0:D4}/{1:D2}/{2:D2}", d / 10000, d % 10000 / 100, d % 100)); } private int Datetime2Int(DateTime dt) { return int.Parse(dt.ToString("yyyyMMdd")); } private bool IsMarketOpenDate(DateTime d) { // 土日 if (d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday) return false; // 年末年始 if ((d.Month == 12 && d.Day == 31) || (d.Month == 1 && d.Day <= 3)) return false; return !IsHoliday(d); } private bool IsHoliday(DateTime d) { // 動かない休日 if ((d.Month == 1 && d.Day == 1) || (d.Month == 1 && d.Day == 2) || (d.Month == 1 && d.Day == 3) || (d.Month == 2 && d.Day == 11) || (d.Month == 4 && d.Day == 29) || (d.Month == 5 && (d.Day >= 3 && d.Day <= 5)) || (d.Month == 11 && (d.Day == 3 || d.Day == 23)) || (d.Month == 12 && d.Day == 23)) return true; if (d.Year >= 2016 && d.Month == 8 && d.Day == 11) // 山の日 return true; // 春分と秋分(1980~2099年に対応) if (d.Month == 3 && d.Day == (int)(20.8431 + 0.242194 * (d.Year - 1980) - (d.Year - 1980) / 4)) return true; if (d.Month == 9 && d.Day == (int)(23.2488 + 0.242194 * (d.Year - 1980) - (d.Year - 1980) / 4)) return true; // ハッピーマンデー if (d.Year < 2000) { if ((d.Month == 1 && d.Day == 15) || (d.Month == 10 && d.Day == 10)) return true; } else { if ((d.Month == 1 || d.Month == 10) && (d.Day >= 8 && d.Day <= 14) && d.DayOfWeek == DayOfWeek.Monday) return true; } if (d.Year < 2003) { if ((d.Month == 7 && d.Day == 20) || (d.Month == 9 && d.Day == 15)) return true; } else { if ((d.Month == 7 || d.Month == 9) && (d.Day >= 15 && d.Day <= 21) && d.DayOfWeek == DayOfWeek.Monday) return true; // シルバーウィーク if (d.Month == 9 && (d.Day == 21 || d.Day == 22) && d.DayOfWeek == DayOfWeek.Tuesday) return IsHoliday(d.AddDays(1)); } // 5月4日が国民の祝日になったので、振替休日が二日以上繰り越す。 if (d.Year > 2007 && d.Month == 5 && d.Day == 6) return d.DayOfWeek >= DayOfWeek.Monday && d.DayOfWeek <= DayOfWeek.Wednesday; //2019年特別対応 if (d.Year == 2019) { if ((d.Month == 4 && d.Day == 30) || (d.Month == 5 && d.Day <= 2)) return true; } // 振り替え休日 if (d.DayOfWeek == DayOfWeek.Monday) return IsHoliday(d.AddDays(-1)); return false; } } }