2021-05-01 panacoran <panacoran@users.osdn.me>
Yahooファイナンスから株価をダウロードできないのを直す
* Protra.Lib/Update/YahooFinanceUpdator.cs (YahooFinanceUpdator): Yahooファイナンスのページ構成の変化に対応
@@ -1,3 +1,9 @@ | ||
1 | +2021-05-01 panacoran <panacoran@users.osdn.me> | |
2 | + | |
3 | + Yahooファイナンスから株価をダウロードできないのを直す | |
4 | + | |
5 | + * Protra.Lib/Update/YahooFinanceUpdator.cs (YahooFinanceUpdator): Yahooファイナンスのページ構成の変化に対応 | |
6 | + | |
1 | 7 | 2021-02-24 darai <darai@users.sourceforge.jp> |
2 | 8 | |
3 | 9 | #41090: 2021年の祝日対応 |
@@ -22,9 +22,11 @@ | ||
22 | 22 | using System.ComponentModel; |
23 | 23 | using System.Globalization; |
24 | 24 | using System.IO; |
25 | +using System.Linq; | |
25 | 26 | using System.Net; |
26 | 27 | using System.Text.RegularExpressions; |
27 | 28 | using Protra.Lib.Data; |
29 | +using Protra.Lib.Lang; | |
28 | 30 | |
29 | 31 | namespace Protra.Lib.Update |
30 | 32 | { |
@@ -80,10 +82,7 @@ | ||
80 | 82 | // 新しいデータが置かれるのは早くても午後7時以降 |
81 | 83 | if (end.Hour < 19) |
82 | 84 | end = end.AddDays(-1); |
83 | - var codes = new List<string>(); | |
84 | - foreach (var brand in GlobalEnv.BrandData) | |
85 | - if ((brand.Flags & Brand.Flag.OBS) == 0) | |
86 | - codes.Add(brand.Code); | |
85 | + var codes = (from brand in GlobalEnv.BrandData where (brand.Flags & Brand.Flag.OBS) == 0 select brand.Code).ToList(); | |
87 | 86 | try |
88 | 87 | { |
89 | 88 | var dates = ListOpenDates(begin, end); |
@@ -97,6 +96,10 @@ | ||
97 | 96 | _progress.Show(worker, dates[0]); |
98 | 97 | // 日経平均の時系列データの存在を確認する。 |
99 | 98 | var n = Math.Min(DaysAtOnce, dates.Count); |
99 | + // 日経平均/TOPICに東証トラブル日のデータが存在するため、 | |
100 | + // 有効なデータを19日分しか取れない | |
101 | + if (IsContainsJpxTrouble(dates[0], dates[n - 1])) | |
102 | + n--; | |
100 | 103 | var nikkei225 = FetchPrices("1001", dates.GetRange(0, n)); |
101 | 104 | if (nikkei225.ReturnStatus != FetchResult.Status.Success) |
102 | 105 | throw new Exception($"株価の取得に失敗しました。時間を置いて再試行してください。: {dates[0]:d}~{dates[n - 1]:d}"); |
@@ -163,6 +166,12 @@ | ||
163 | 166 | } |
164 | 167 | } |
165 | 168 | |
169 | + private static bool IsContainsJpxTrouble(DateTime begin, DateTime end) | |
170 | + { | |
171 | + var theDate = new DateTime(2020, 10, 1); | |
172 | + return theDate.CompareTo(begin) >= 0 && theDate.CompareTo(end) <= 0; | |
173 | + } | |
174 | + | |
166 | 175 | private FetchResult FetchPrices(string code, IList<DateTime> dates) |
167 | 176 | { |
168 | 177 | var status = GetPage(code, dates[0], dates[dates.Count - 1], out var page); |
@@ -173,12 +182,13 @@ | ||
173 | 182 | |
174 | 183 | private FetchResult.Status GetPage(string code, DateTime begin, DateTime end, out string page) |
175 | 184 | { |
176 | - if (code == "1001") | |
177 | - code = "998407"; | |
178 | - else if (code == "1002") | |
179 | - code = "998405"; | |
180 | - var dl = new DownloadUtil( | |
181 | - $"https://info.finance.yahoo.co.jp/history/?code={code}&sy={begin.Year}&sm={begin.Month}&sd={begin.Day}&ey={end.Year}&em={end.Month}&ed={end.Day}&tm=d"); | |
185 | + string codeString = code; | |
186 | + if (code == "1001" || code == "1002") | |
187 | + codeString = code == "1001" ? "998407.O" : "998405.T"; | |
188 | + var oldUrl = $"https://info.finance.yahoo.co.jp/history/?code={codeString}&sy={begin.Year}&sm={begin.Month}&sd={begin.Day}&ey={end.Year}&em={end.Month}&ed={end.Day}&tm=d"; | |
189 | + var url = $"https://finance.yahoo.co.jp/quote/{codeString}.T/history?from={begin:yyyyMMdd}&to={end:yyyyMMdd}&timeFrame=d&page=1"; | |
190 | + retry: | |
191 | + var dl = new DownloadUtil(url); | |
182 | 192 | page = null; |
183 | 193 | try |
184 | 194 | { |
@@ -193,9 +203,18 @@ | ||
193 | 203 | switch (e.Status) |
194 | 204 | { |
195 | 205 | case WebExceptionStatus.ProtocolError: |
196 | - var c = ((HttpWebResponse)e.Response).StatusCode; | |
197 | - if (c == HttpStatusCode.BadGateway || c == HttpStatusCode.InternalServerError) | |
198 | - goto case WebExceptionStatus.Timeout; | |
206 | + switch (((HttpWebResponse)e.Response).StatusCode) | |
207 | + { | |
208 | + case (HttpStatusCode)999: | |
209 | + case HttpStatusCode.InternalServerError: | |
210 | + case HttpStatusCode.BadGateway: | |
211 | + return FetchResult.Status.Retry; | |
212 | + case HttpStatusCode.NotFound: | |
213 | + if (url == oldUrl) | |
214 | + return FetchResult.Status.Failure; | |
215 | + url = oldUrl; | |
216 | + goto retry; | |
217 | + } | |
199 | 218 | throw; |
200 | 219 | case WebExceptionStatus.Timeout: |
201 | 220 | case WebExceptionStatus.ConnectionClosed: |
@@ -210,27 +229,41 @@ | ||
210 | 229 | return FetchResult.Status.Success; |
211 | 230 | } |
212 | 231 | |
232 | + private static readonly Regex Valid = new Regex( | |
233 | + @"<tr[^>]*><th[^>]*>(?<year>\d{4})年(?<month>1?\d)月(?<day>\d?\d)日<\/th><td[^>]+>(?:<span[^>]+>)+(?<open>[0-9,.]+)<\/span>.*?<\/td><td[^>]+>(?:<span[^>]+>)+(?<high>[0-9,.]+)<\/span>.*?<\/td><td[^>]+>(?:<span[^>]+>)+(?<low>[0-9,.]+)<\/span>.+?<\/td><td[^>]+>(?:<span[^>]+>)+(?<close>[0-9,.]+)<\/span>.+?<\/td>(?:<td.*?>(?<volume>[0-9,.]+)<\/span>.+?<\/td>)?<\/tr>", | |
234 | + RegexOptions.Compiled); | |
235 | + | |
236 | + private static readonly Regex NoData = new Regex("時系列情報がありません"); | |
237 | + | |
238 | + private static readonly Regex ValidOld = new Regex( | |
239 | + @"<td>(?<year>\d{4})年(?<month>1?\d)月(?<day>\d?\d)日</td><td>(?<open>[0-9,.]+)</td><td>(?<high>[0-9,.]+)</td><td>(?<low>[0-9,.]+)</td><td>(?<close>[0-9,.]+)</td>(?:<td>(?<volume>[0-9,]+)</td>)?", RegexOptions.Compiled); | |
240 | + | |
241 | + private static readonly Regex NoDataOld = new Regex("該当する期間のデータはありません。<br>期間をご確認ください。"); | |
242 | + | |
243 | + private static readonly Regex Obs = | |
244 | + new Regex("該当する銘柄はありません。<br>再度銘柄(コード)を入力し、「表示」ボタンを押してください。", RegexOptions.Compiled); | |
245 | + | |
246 | + private static readonly Regex Empty = new Regex("<dl class=\"stocksInfo\">\n<dt></dt><dd class=\"category yjSb\"></dd>", RegexOptions.Compiled); | |
247 | + | |
213 | 248 | private FetchResult ParsePage(string code, string buf, IEnumerable<DateTime> dates) |
214 | 249 | { |
215 | - var valid = new Regex( | |
216 | - @"<td>(?<year>\d{4})年(?<month>1?\d)月(?<day>\d?\d)日</td>" + | |
217 | - "<td>(?<open>[0-9,.]+)</td><td>(?<high>[0-9,.]+)</td><td>(?<low>[0-9,.]+)</td>" + | |
218 | - "<td>(?<close>[0-9,.]+)</td>(?:<td>(?<volume>[0-9,]+)</td>)?"); | |
219 | - var invalid = new Regex("該当する期間のデータはありません。<br>期間をご確認ください。"); | |
220 | - var obs = new Regex("該当する銘柄はありません。<br>再度銘柄(コード)を入力し、「表示」ボタンを押してください。"); | |
221 | - var empty = new Regex("<dl class=\"stocksInfo\">\n<dt></dt><dd class=\"category yjSb\"></dd>"); | |
222 | - | |
223 | 250 | var dict = new Dictionary<DateTime, Price>(); |
224 | - var matches = valid.Matches(buf); | |
251 | + var matches = Valid.Matches(buf); | |
225 | 252 | if (matches.Count == 0) |
226 | 253 | { |
227 | - if (obs.Match(buf).Success || empty.Match(buf).Success) // 上場廃止(銘柄データが空のこともある) | |
228 | - return new FetchResult {ReturnStatus = FetchResult.Status.Obsolete}; | |
229 | - if (!invalid.Match(buf).Success) | |
230 | - throw new Exception("ページから株価を取得できません。"); | |
231 | - // ここに到達するのは出来高がないか株価が用意されていない場合 | |
254 | + if (!NoData.IsMatch(buf)) | |
255 | + { | |
256 | + matches = ValidOld.Matches(buf); | |
257 | + if (matches.Count == 0) | |
258 | + { | |
259 | + if (Obs.Match(buf).Success || Empty.Match(buf).Success) // 上場廃止(銘柄データが空のこともある) | |
260 | + return new FetchResult {ReturnStatus = FetchResult.Status.Obsolete}; | |
261 | + if (!NoDataOld.IsMatch(buf)) | |
262 | + throw new Exception("ページから株価を取得できません。"); | |
263 | + // ここに到達するのは出来高がないか株価が用意されていない場合 | |
264 | + } | |
265 | + } | |
232 | 266 | } |
233 | - | |
234 | 267 | try |
235 | 268 | { |
236 | 269 | const NumberStyles s = NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands; |
@@ -257,12 +290,9 @@ | ||
257 | 290 | } |
258 | 291 | |
259 | 292 | // 出来高がない日の株価データがないので値が0のデータを補う。 |
260 | - var prices = new List<Price>(); | |
261 | - foreach (var date in dates) | |
262 | - { | |
263 | - prices.Add(dict.TryGetValue(date, out var price) ? price : new Price {Date = date, Code = code}); | |
264 | - } | |
265 | - | |
293 | + var prices = dates | |
294 | + .Select(date => dict.TryGetValue(date, out var price) ? price : new Price {Date = date, Code = code}) | |
295 | + .ToList(); | |
266 | 296 | return new FetchResult {Code = code, Prices = prices, ReturnStatus = FetchResult.Status.Success}; |
267 | 297 | } |
268 | 298 |