kansai_train_info 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +14 -17
- data/README.md +148 -30
- data/lib/kansai_train_info/cli.rb +4 -7
- data/lib/kansai_train_info/client.rb +157 -44
- data/lib/kansai_train_info/configuration.rb +33 -0
- data/lib/kansai_train_info/errors.rb +13 -0
- data/lib/kansai_train_info/http_client.rb +44 -0
- data/lib/kansai_train_info/parser.rb +24 -0
- data/lib/kansai_train_info/route.rb +72 -0
- data/lib/kansai_train_info/script.rb +16 -6
- data/lib/kansai_train_info/status_formatter.rb +48 -0
- data/lib/kansai_train_info/version.rb +1 -1
- data/lib/kansai_train_info.rb +10 -4
- metadata +19 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11a5e91f5bf0a274013d50d1f79ffb8dca78683fea2a724602be61f7c6986baa
|
|
4
|
+
data.tar.gz: 557caaccd6c84d19ebc93b17b399cd985d9631cb8f622f328bac6a6ed6418c46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56f319c4be59afc3fc01baa6c7f67ecd6b5b5179f8a489864b38b2eecce6ff598f3a8d7417f61ad0649d74bb5b2c4aa84cc7234f0be3a4041cc9d43d2d6141fd
|
|
7
|
+
data.tar.gz: 862d0ddc6c454c6da87579e7f6f6bd6ea17af1f2601d7e2593b6732f0c3e9f799f3adbee187e5873969e7ae0b8f93eda7e4ebc6f0030f4056744698ff409376a
|
data/LICENSE.txt
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
kansai_train_info is licensed under the GPLv3 license for all open source applications.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Please do not use this project for commercial use, it is not intended to be used for commercial use.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
5
|
+
Copyright (C) 2020 okamotchan
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
This program is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
data/README.md
CHANGED
|
@@ -1,54 +1,172 @@
|
|
|
1
1
|
# kansai_train_info
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://dl.circleci.com/status-badge/redirect/gh/o8n/kansai_train_info/tree/master)
|
|
4
|
+
[](https://badge.fury.io/rb/kansai_train_info)
|
|
5
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
6
|
+
|
|
7
|
+
関西地方の鉄道運行情報を取得するRuby Gemです。
|
|
8
|
+
|
|
9
|
+
[English](README.en.md)
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🚃 関西主要路線の運行情報をリアルタイム取得
|
|
14
|
+
- 🔄 自動リトライ機能(指数バックオフ付き)
|
|
15
|
+
- ⚡ 高速なHTTP通信とHTML解析
|
|
16
|
+
- 🛡️ 包括的なエラーハンドリング
|
|
17
|
+
- 📝 完全な型定義(RBS)
|
|
18
|
+
- 🎯 90%以上のテストカバレッジ
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Gemをインストール:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gem install kansai_train_info
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
または、Gemfileに追加:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
gem 'kansai_train_info', '~> 0.2.0'
|
|
32
|
+
```
|
|
4
33
|
|
|
5
34
|
## Usage
|
|
6
35
|
|
|
36
|
+
### Ruby
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require 'kansai_train_info'
|
|
40
|
+
|
|
41
|
+
# 単一路線の情報を取得
|
|
42
|
+
KansaiTrainInfo.get(['大阪環状線'])
|
|
43
|
+
# => 大阪環状線は平常運転です
|
|
44
|
+
|
|
45
|
+
# 複数路線の情報を取得
|
|
46
|
+
KansaiTrainInfo.get(['大阪環状線', '御堂筋線'])
|
|
47
|
+
# => "大阪環状線は列車遅延があります。10分程度の遅れが発生しています。"
|
|
48
|
+
|
|
49
|
+
# 詳細URLを含める
|
|
50
|
+
KansaiTrainInfo.get(['大阪環状線'], url: true)
|
|
51
|
+
# => "大阪環状線は列車遅延があります。https://transit.yahoo.co.jp/traininfo/detail/263/0/"
|
|
52
|
+
|
|
53
|
+
# 利用可能な路線を表示
|
|
54
|
+
KansaiTrainInfo.help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### CLI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 単一路線
|
|
61
|
+
kansai_train_info get 大阪環状線
|
|
62
|
+
|
|
63
|
+
# 複数路線
|
|
64
|
+
kansai_train_info get 大阪環状線 御堂筋線
|
|
65
|
+
|
|
66
|
+
# URLを含める
|
|
67
|
+
kansai_train_info get 大阪環状線 --url
|
|
68
|
+
|
|
69
|
+
# ヘルプ
|
|
70
|
+
kansai_train_info help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Supported Lines
|
|
74
|
+
|
|
75
|
+
- 大阪環状線
|
|
76
|
+
- 近鉄京都線
|
|
77
|
+
- 阪急京都線
|
|
78
|
+
- 御堂筋線
|
|
79
|
+
- 烏丸線
|
|
80
|
+
- 東西線
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
カスタム設定が可能です:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
KansaiTrainInfo.configure do |config|
|
|
88
|
+
config.timeout = 30 # タイムアウト時間(秒)
|
|
89
|
+
config.max_retries = 5 # 最大リトライ回数
|
|
90
|
+
config.retry_delay = 2 # リトライ間隔の基準時間(秒)
|
|
91
|
+
config.user_agent = 'MyApp/1.0'
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
詳細は[Configuration Guide](docs/CONFIGURATION.md)を参照してください。
|
|
96
|
+
|
|
97
|
+
## Requirements
|
|
98
|
+
|
|
99
|
+
- Ruby >= 3.0.0
|
|
7
100
|
|
|
8
|
-
|
|
101
|
+
## Development
|
|
9
102
|
|
|
10
|
-
|
|
103
|
+
### Testing
|
|
11
104
|
|
|
12
|
-
|
|
105
|
+
Run tests with coverage report:
|
|
13
106
|
|
|
107
|
+
```sh
|
|
108
|
+
# Run all tests
|
|
109
|
+
bundle exec rspec
|
|
110
|
+
|
|
111
|
+
# Run specific test file
|
|
112
|
+
bundle exec rspec spec/kansai_train_info/client_spec.rb
|
|
14
113
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
=> true
|
|
18
|
-
irb(main):002:0> KansaiTrainInfo.get(['大阪環状線'])
|
|
19
|
-
大阪環状線は平常運転です
|
|
114
|
+
# View coverage report
|
|
115
|
+
open coverage/index.html
|
|
20
116
|
```
|
|
21
117
|
|
|
22
|
-
|
|
118
|
+
The project maintains a minimum test coverage of 90%.
|
|
23
119
|
|
|
24
|
-
|
|
120
|
+
### Type Checking
|
|
25
121
|
|
|
26
|
-
|
|
122
|
+
This gem uses RBS for type definitions and Steep for type checking.
|
|
27
123
|
|
|
124
|
+
```sh
|
|
125
|
+
# Run type checking
|
|
126
|
+
bundle exec steep check
|
|
28
127
|
|
|
29
|
-
|
|
128
|
+
# Or use rake task
|
|
129
|
+
bundle exec rake steep
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
### Linting
|
|
30
134
|
|
|
31
|
-
|
|
135
|
+
Rubocopでコードスタイルをチェック:
|
|
32
136
|
|
|
33
137
|
```sh
|
|
34
|
-
|
|
35
|
-
Traceback (most recent call last):
|
|
36
|
-
6: from /Users/name/.rbenv/versions/2.7.1/bin/irb:23:in `<main>'
|
|
37
|
-
5: from /Users/name/.rbenv/versions/2.7.1/bin/irb:23:in `load'
|
|
38
|
-
4: from /Users/name/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
|
|
39
|
-
3: from (irb):1
|
|
40
|
-
2: from /Users/name/.rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require'
|
|
41
|
-
1: from /Users/name/.rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require'
|
|
42
|
-
LoadError (cannot load such file -- KansaiTrainInfo)
|
|
138
|
+
bundle exec rubocop
|
|
43
139
|
```
|
|
44
140
|
|
|
45
|
-
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
- [API Documentation](docs/API.md) - 詳細なAPIリファレンス ([English](docs/API.en.md))
|
|
144
|
+
- [Configuration Guide](docs/CONFIGURATION.md) - 設定オプションの詳細 ([English](docs/CONFIGURATION.en.md))
|
|
145
|
+
- [Contributing Guide](CONTRIBUTING.md) - 貢献方法 ([English](CONTRIBUTING.en.md))
|
|
146
|
+
- [Changelog](CHANGELOG.md) - 変更履歴
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
このgemは以下のカスタムエラーを提供します:
|
|
46
151
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
152
|
+
- `KansaiTrainInfo::NetworkError` - ネットワーク関連のエラー
|
|
153
|
+
- `KansaiTrainInfo::TimeoutError` - タイムアウトエラー
|
|
154
|
+
- `KansaiTrainInfo::ParseError` - HTML解析エラー
|
|
155
|
+
- `KansaiTrainInfo::InvalidRouteError` - 無効な路線名エラー
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
begin
|
|
159
|
+
KansaiTrainInfo.get(['存在しない路線'])
|
|
160
|
+
rescue KansaiTrainInfo::InvalidRouteError => e
|
|
161
|
+
puts "エラー: #{e.message}"
|
|
162
|
+
end
|
|
52
163
|
```
|
|
53
164
|
|
|
54
|
-
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
kansai_train_info is licensed under the GPLv3 license for all open source applications.
|
|
168
|
+
|
|
169
|
+
Please do not use this project for commercial use, it is not intended to be used for commercial use.
|
|
170
|
+
|
|
171
|
+
See [LICENSE.txt](LICENSE.txt) for details.
|
|
172
|
+
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
module KansaiTrainInfo
|
|
2
2
|
class CLI < Thor
|
|
3
|
-
desc
|
|
3
|
+
desc 'KansaiTrainInfo *routes', 'Get'
|
|
4
4
|
def get(*routes)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
print text
|
|
8
|
-
print "\n"
|
|
9
|
-
end
|
|
5
|
+
result = KansaiTrainInfo.get(routes)
|
|
6
|
+
puts result if result
|
|
10
7
|
end
|
|
11
8
|
|
|
12
|
-
desc
|
|
9
|
+
desc 'KansaiTrainInfo help', 'Help'
|
|
13
10
|
def help
|
|
14
11
|
KansaiTrainInfo.help
|
|
15
12
|
end
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'timeout'
|
|
3
5
|
require 'nokogiri'
|
|
6
|
+
require 'kansai_train_info/errors'
|
|
7
|
+
require 'kansai_train_info/configuration'
|
|
8
|
+
require 'kansai_train_info/http_client'
|
|
9
|
+
require 'kansai_train_info/parser'
|
|
10
|
+
require 'kansai_train_info/route'
|
|
11
|
+
require 'kansai_train_info/status_formatter'
|
|
4
12
|
|
|
13
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
14
|
module KansaiTrainInfo
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
6
16
|
class << self
|
|
17
|
+
# Legacy constants for backward compatibility
|
|
7
18
|
LINES = {
|
|
8
19
|
大阪環状線: [4, 2, 263],
|
|
9
20
|
近鉄京都線: [6, 5, 288],
|
|
@@ -13,72 +24,174 @@ module KansaiTrainInfo
|
|
|
13
24
|
東西線: [34, 3, 319]
|
|
14
25
|
}.freeze
|
|
15
26
|
|
|
16
|
-
|
|
27
|
+
DEFAULT_TIMEOUT = 10
|
|
28
|
+
MAX_RETRIES = 3
|
|
29
|
+
|
|
30
|
+
# 指定された路線の運行情報を取得する
|
|
31
|
+
#
|
|
32
|
+
# @param route_array [Array<String>] 取得したい路線名の配列
|
|
33
|
+
# @param url [Boolean] 詳細URLを含めるかどうか(デフォルト: false)
|
|
34
|
+
# @return [String, nil] 運行情報のメッセージ。正常運転の場合はnil
|
|
35
|
+
# @raise [InvalidRouteError] 無効な路線名が指定された場合
|
|
36
|
+
#
|
|
37
|
+
# @example 単一路線の情報を取得
|
|
38
|
+
# KansaiTrainInfo.get(['大阪環状線'])
|
|
39
|
+
#
|
|
40
|
+
# @example 複数路線でURLを含める
|
|
41
|
+
# KansaiTrainInfo.get(['大阪環状線', '御堂筋線'], url: true)
|
|
42
|
+
#
|
|
43
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
17
44
|
def get(route_array, url: false)
|
|
45
|
+
if route_array.empty?
|
|
46
|
+
puts '利用可能な路線を入力してください'
|
|
47
|
+
return nil
|
|
48
|
+
end
|
|
49
|
+
|
|
18
50
|
messages = []
|
|
19
51
|
|
|
20
|
-
route_array.each do |
|
|
21
|
-
|
|
22
|
-
raise
|
|
52
|
+
route_array.each do |route_name|
|
|
53
|
+
route = route_registry.find(route_name)
|
|
54
|
+
raise InvalidRouteError, "Invalid route: #{route_name}" unless route
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
begin
|
|
57
|
+
status = fetch_route_status(route)
|
|
58
|
+
description = status && status != '平常運転' ? fetch_description(route.detail_url) : nil
|
|
59
|
+
|
|
60
|
+
formatter = StatusFormatter.new(route_name, status)
|
|
61
|
+
formatted_message = formatter.format(
|
|
62
|
+
include_url: url,
|
|
63
|
+
detail_url: route.detail_url,
|
|
64
|
+
description: description
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
messages << formatted_message if formatted_message
|
|
68
|
+
rescue NetworkError => e
|
|
69
|
+
messages << "#{route_name}: ネットワークエラー - #{e.message}"
|
|
70
|
+
rescue ParseError => e
|
|
71
|
+
messages << "#{route_name}: データ解析エラー - #{e.message}"
|
|
72
|
+
end
|
|
33
73
|
end
|
|
74
|
+
|
|
75
|
+
return nil if messages.empty?
|
|
76
|
+
|
|
77
|
+
messages.compact.join(', ')
|
|
34
78
|
end
|
|
35
|
-
# rubocop:enable Metrics/
|
|
79
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
36
80
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
# 利用可能な路線を表示する
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# KansaiTrainInfo.help
|
|
87
|
+
# # => 利用可能な路線:
|
|
88
|
+
# # => 大阪環状線、近鉄京都線、阪急京都線, 御堂筋線, 烏丸線, 東西線
|
|
89
|
+
def help
|
|
90
|
+
help_message = "利用可能な路線:\n#{route_registry.names.join('、')}"
|
|
91
|
+
puts help_message
|
|
92
|
+
end
|
|
40
93
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def route_registry
|
|
97
|
+
@route_registry ||= RouteRegistry.new
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def http_client
|
|
101
|
+
@http_client ||= HttpClient.new
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def fetch_route_status(route)
|
|
105
|
+
url = "#{KansaiTrainInfo.configuration.base_url}/traininfo/area/6/"
|
|
106
|
+
html = http_client.get(url)
|
|
107
|
+
parser = Parser.new(html)
|
|
108
|
+
parser.extract_status(route.status_xpath)
|
|
109
|
+
end
|
|
45
110
|
|
|
46
|
-
|
|
111
|
+
def fetch_description(detail_url)
|
|
112
|
+
html = http_client.get(detail_url)
|
|
113
|
+
parser = Parser.new(html)
|
|
114
|
+
parser.extract_detail('//*[@id="mdServiceStatus"]/dl/dd/p')
|
|
115
|
+
rescue NetworkError => e
|
|
116
|
+
"詳細情報取得エラー: #{e.message}"
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
"詳細情報解析エラー: #{e.message}"
|
|
47
119
|
end
|
|
48
120
|
|
|
121
|
+
# Legacy method for backward compatibility
|
|
49
122
|
def description(detail_url)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
detail_doc.xpath('//*[@id="mdServiceStatus"]/dl/dd/p').first.text
|
|
123
|
+
html = http_client.get(detail_url)
|
|
124
|
+
parser = Parser.new(html)
|
|
125
|
+
result = parser.extract_detail('//*[@id="mdServiceStatus"]/dl/dd/p')
|
|
126
|
+
result || '詳細情報を取得できませんでした'
|
|
127
|
+
rescue StandardError
|
|
128
|
+
'詳細情報を取得できませんでした'
|
|
57
129
|
end
|
|
58
130
|
|
|
59
|
-
#
|
|
131
|
+
# Legacy method for backward compatibility
|
|
60
132
|
def message(route, state, url, detail_url)
|
|
61
133
|
return "#{route}は運行情報がありません" if state.nil?
|
|
62
134
|
|
|
63
|
-
|
|
64
|
-
state
|
|
65
|
-
puts "#{route}は#{
|
|
66
|
-
message = case
|
|
135
|
+
# Remove status indicators without modifying the original string
|
|
136
|
+
clean_state = state.gsub('[○]', '').gsub('[!]', '')
|
|
137
|
+
puts "#{route}は#{clean_state}です" if clean_state == '平常運転'
|
|
138
|
+
message = case clean_state
|
|
67
139
|
when '運転状況'
|
|
68
|
-
"#{route}は#{
|
|
140
|
+
"#{route}は#{clean_state}に変更があります。"
|
|
69
141
|
when '列車遅延'
|
|
70
|
-
"#{route}は#{
|
|
142
|
+
"#{route}は#{clean_state}があります。"
|
|
71
143
|
when '運転見合わせ'
|
|
72
|
-
"#{route}は#{
|
|
144
|
+
"#{route}は#{clean_state}しています。"
|
|
73
145
|
end
|
|
74
|
-
|
|
146
|
+
return "#{route}は#{clean_state}です" if message.nil?
|
|
147
|
+
|
|
148
|
+
desc = description(detail_url)
|
|
149
|
+
show_message = "#{message} #{desc}"
|
|
75
150
|
url ? show_message + detail_url : show_message
|
|
76
151
|
end
|
|
77
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
78
152
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
153
|
+
# Legacy method for backward compatibility
|
|
154
|
+
# rubocop:disable Metrics/AbcSize
|
|
155
|
+
def fetch_url(url, retries = 0)
|
|
156
|
+
uri = URI.parse(url)
|
|
157
|
+
|
|
158
|
+
Timeout.timeout(KansaiTrainInfo.configuration.timeout) do
|
|
159
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
160
|
+
request = Net::HTTP::Get.new(uri)
|
|
161
|
+
request['User-Agent'] = KansaiTrainInfo.configuration.user_agent
|
|
162
|
+
response = http.request(request)
|
|
163
|
+
|
|
164
|
+
case response
|
|
165
|
+
when Net::HTTPSuccess
|
|
166
|
+
response.body.dup.force_encoding('UTF-8')
|
|
167
|
+
else
|
|
168
|
+
raise NetworkError, "HTTP Error: #{response.code} #{response.message}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
rescue Timeout::Error
|
|
173
|
+
raise TimeoutError, "Request timeout after #{KansaiTrainInfo.configuration.timeout} seconds"
|
|
174
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
175
|
+
unless retries < KansaiTrainInfo.configuration.max_retries
|
|
176
|
+
raise NetworkError, "Connection failed after #{KansaiTrainInfo.configuration.max_retries} retries: #{e.message}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
sleep(KansaiTrainInfo.configuration.retry_delays[retries])
|
|
180
|
+
fetch_url(url, retries + 1)
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
183
|
+
end
|
|
184
|
+
# rubocop:enable Metrics/AbcSize
|
|
185
|
+
|
|
186
|
+
# Legacy method for backward compatibility
|
|
187
|
+
def kansai_doc
|
|
188
|
+
url = "#{KansaiTrainInfo.configuration.base_url}/traininfo/area/6/"
|
|
189
|
+
html = fetch_url(url)
|
|
190
|
+
Nokogiri::HTML.parse(html, nil, 'utf-8')
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
raise ParseError, "HTMLの解析に失敗しました: #{e.message}"
|
|
82
193
|
end
|
|
83
194
|
end
|
|
195
|
+
# rubocop:enable Metrics/ClassLength
|
|
84
196
|
end
|
|
197
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KansaiTrainInfo
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :user_agent, :timeout, :max_retries, :retry_delay, :base_url
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@user_agent = "kansai_train_info/#{VERSION}"
|
|
9
|
+
@timeout = 10
|
|
10
|
+
@max_retries = 3
|
|
11
|
+
@retry_delay = 1
|
|
12
|
+
@base_url = 'https://transit.yahoo.co.jp'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def retry_delays
|
|
16
|
+
(0...@max_retries).map { |i| @retry_delay * (2**i) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def configuration
|
|
22
|
+
@configuration ||= Configuration.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configure
|
|
26
|
+
yield(configuration)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset_configuration!
|
|
30
|
+
@configuration = Configuration.new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KansaiTrainInfo
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class NetworkError < Error; end
|
|
7
|
+
|
|
8
|
+
class TimeoutError < NetworkError; end
|
|
9
|
+
|
|
10
|
+
class ParseError < Error; end
|
|
11
|
+
|
|
12
|
+
class InvalidRouteError < Error; end
|
|
13
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
module KansaiTrainInfo
|
|
7
|
+
class HttpClient
|
|
8
|
+
def initialize(config = KansaiTrainInfo.configuration)
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/AbcSize
|
|
13
|
+
def get(url, retries: 0)
|
|
14
|
+
uri = URI.parse(url)
|
|
15
|
+
|
|
16
|
+
Timeout.timeout(@config.timeout) do
|
|
17
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
18
|
+
request = Net::HTTP::Get.new(uri)
|
|
19
|
+
request['User-Agent'] = @config.user_agent
|
|
20
|
+
response = http.request(request)
|
|
21
|
+
|
|
22
|
+
case response
|
|
23
|
+
when Net::HTTPSuccess
|
|
24
|
+
response.body.dup.force_encoding('UTF-8')
|
|
25
|
+
else
|
|
26
|
+
raise NetworkError, "HTTP Error: #{response.code} #{response.message}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
rescue Timeout::Error
|
|
31
|
+
raise TimeoutError, "Request timeout after #{@config.timeout} seconds"
|
|
32
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
|
33
|
+
unless retries < @config.max_retries
|
|
34
|
+
raise NetworkError, "Connection failed after #{@config.max_retries} retries: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sleep(@config.retry_delays[retries])
|
|
38
|
+
get(url, retries: retries + 1)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
# rubocop:enable Metrics/AbcSize
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
|
|
5
|
+
module KansaiTrainInfo
|
|
6
|
+
class Parser
|
|
7
|
+
attr_reader :document
|
|
8
|
+
|
|
9
|
+
def initialize(html)
|
|
10
|
+
@document = Nokogiri::HTML.parse(html, nil, 'utf-8')
|
|
11
|
+
rescue StandardError => e
|
|
12
|
+
raise ParseError, "HTMLの解析に失敗しました: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def extract_status(xpath)
|
|
16
|
+
@document.xpath(xpath).first&.text
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def extract_detail(xpath)
|
|
20
|
+
element = @document.xpath(xpath).first
|
|
21
|
+
element&.text || '詳細情報を取得できませんでした'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KansaiTrainInfo
|
|
4
|
+
class Route
|
|
5
|
+
attr_reader :name, :area_index, :row_index, :detail_id
|
|
6
|
+
|
|
7
|
+
def initialize(name:, area_index:, row_index:, detail_id:)
|
|
8
|
+
@name = name
|
|
9
|
+
@area_index = area_index
|
|
10
|
+
@row_index = row_index
|
|
11
|
+
@detail_id = detail_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def status_xpath
|
|
15
|
+
"//*[@id='mdAreaMajorLine']/div[#{area_index}]/table/tr[#{row_index}]/td[2]"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def detail_url
|
|
19
|
+
"#{KansaiTrainInfo.configuration.base_url}/traininfo/detail/#{detail_id}/0/"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_sym
|
|
23
|
+
name.to_sym
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class RouteRegistry
|
|
28
|
+
def initialize
|
|
29
|
+
@routes = {}
|
|
30
|
+
register_default_routes
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def register(name, area_index:, row_index:, detail_id:)
|
|
34
|
+
route = Route.new(
|
|
35
|
+
name: name,
|
|
36
|
+
area_index: area_index,
|
|
37
|
+
row_index: row_index,
|
|
38
|
+
detail_id: detail_id
|
|
39
|
+
)
|
|
40
|
+
@routes[name.to_sym] = route
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def find(name)
|
|
44
|
+
@routes[name.to_sym]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def all
|
|
48
|
+
@routes.values
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def names
|
|
52
|
+
@routes.keys.map(&:to_s)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def register_default_routes
|
|
58
|
+
register('大阪環状線', area_index: 4, row_index: 2, detail_id: 263)
|
|
59
|
+
register('近鉄京都線', area_index: 6, row_index: 5, detail_id: 288)
|
|
60
|
+
register('阪急京都線', area_index: 8, row_index: 2, detail_id: 306)
|
|
61
|
+
register('御堂筋線', area_index: 10, row_index: 3, detail_id: 321)
|
|
62
|
+
register('烏丸線', area_index: 34, row_index: 2, detail_id: 318)
|
|
63
|
+
register('東西線', area_index: 34, row_index: 3, detail_id: 319)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class << self
|
|
68
|
+
def route_registry
|
|
69
|
+
@route_registry ||= RouteRegistry.new
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'nokogiri'
|
|
4
|
-
require '
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'timeout'
|
|
5
6
|
|
|
6
7
|
url = 'https://transit.yahoo.co.jp/traininfo/area/6/'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
uri = URI.parse(url)
|
|
10
|
+
html = Timeout.timeout(10) do
|
|
11
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
12
|
+
request = Net::HTTP::Get.new(uri)
|
|
13
|
+
response = http.request(request)
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
case response
|
|
16
|
+
when Net::HTTPSuccess
|
|
17
|
+
# Create a mutable copy before force_encoding
|
|
18
|
+
response.body.dup.force_encoding('UTF-8')
|
|
19
|
+
else
|
|
20
|
+
raise "HTTP Error: #{response.code} #{response.message}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
13
23
|
end
|
|
14
24
|
|
|
15
|
-
doc = Nokogiri::HTML.parse(html, nil,
|
|
25
|
+
doc = Nokogiri::HTML.parse(html, nil, 'utf-8')
|
|
16
26
|
|
|
17
27
|
status_xpath = "//*[@id='mdAreaMajorLine']/div[4]/table/tr[3]/td[1]"
|
|
18
28
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KansaiTrainInfo
|
|
4
|
+
class StatusFormatter
|
|
5
|
+
STATUS_INDICATORS = {
|
|
6
|
+
normal: '[○]',
|
|
7
|
+
alert: '[!]'
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
STATUS_MESSAGES = {
|
|
11
|
+
'運転状況' => ->(route) { "#{route}は運転状況に変更があります。" },
|
|
12
|
+
'列車遅延' => ->(route) { "#{route}は列車遅延があります。" },
|
|
13
|
+
'運転見合わせ' => ->(route) { "#{route}は運転見合わせしています。" }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(route_name, raw_status)
|
|
17
|
+
@route_name = route_name
|
|
18
|
+
@raw_status = raw_status
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def format(include_url: false, detail_url: nil, description: nil)
|
|
22
|
+
return "#{@route_name}は運行情報がありません" if @raw_status.nil?
|
|
23
|
+
|
|
24
|
+
status = clean_status(@raw_status)
|
|
25
|
+
|
|
26
|
+
if status == '平常運転'
|
|
27
|
+
puts "#{@route_name}は#{status}です"
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
message = build_message(status, description)
|
|
32
|
+
include_url && detail_url ? "#{message}#{detail_url}" : message
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def clean_status(raw_status)
|
|
38
|
+
STATUS_INDICATORS.values.reduce(raw_status) do |status, indicator|
|
|
39
|
+
status.gsub(indicator, '')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_message(status, description)
|
|
44
|
+
base_message = STATUS_MESSAGES[status]&.call(@route_name) || "#{@route_name}は#{status}です"
|
|
45
|
+
description ? "#{base_message} #{description}" : base_message
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/kansai_train_info.rb
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
require 'thor'
|
|
2
|
+
require 'kansai_train_info/version'
|
|
3
|
+
require 'kansai_train_info/errors'
|
|
4
|
+
require 'kansai_train_info/configuration'
|
|
5
|
+
require 'kansai_train_info/http_client'
|
|
6
|
+
require 'kansai_train_info/parser'
|
|
7
|
+
require 'kansai_train_info/route'
|
|
8
|
+
require 'kansai_train_info/status_formatter'
|
|
9
|
+
require 'kansai_train_info/client'
|
|
10
|
+
require 'kansai_train_info/cli'
|
|
5
11
|
|
|
6
12
|
module KansaiTrainInfo
|
|
7
13
|
end
|
metadata
CHANGED
|
@@ -1,85 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kansai_train_info
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- o8n
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-07-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '1.15'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- - "
|
|
24
|
+
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '1.15'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: thor
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: bundler
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - ">="
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0'
|
|
48
|
-
type: :development
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - ">="
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: rake
|
|
57
29
|
requirement: !ruby/object:Gem::Requirement
|
|
58
30
|
requirements:
|
|
59
31
|
- - "~>"
|
|
60
32
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
62
|
-
type: :
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
63
35
|
prerelease: false
|
|
64
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
37
|
requirements:
|
|
66
38
|
- - "~>"
|
|
67
39
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: rspec
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - ">="
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
76
|
-
type: :development
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ">="
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
40
|
+
version: '1.0'
|
|
83
41
|
description: you can check train info in kansai area
|
|
84
42
|
email:
|
|
85
43
|
- m.okamotchan@gmail.com
|
|
@@ -92,12 +50,19 @@ files:
|
|
|
92
50
|
- lib/kansai_train_info.rb
|
|
93
51
|
- lib/kansai_train_info/cli.rb
|
|
94
52
|
- lib/kansai_train_info/client.rb
|
|
53
|
+
- lib/kansai_train_info/configuration.rb
|
|
54
|
+
- lib/kansai_train_info/errors.rb
|
|
55
|
+
- lib/kansai_train_info/http_client.rb
|
|
56
|
+
- lib/kansai_train_info/parser.rb
|
|
57
|
+
- lib/kansai_train_info/route.rb
|
|
95
58
|
- lib/kansai_train_info/script.rb
|
|
59
|
+
- lib/kansai_train_info/status_formatter.rb
|
|
96
60
|
- lib/kansai_train_info/version.rb
|
|
97
61
|
homepage:
|
|
98
62
|
licenses:
|
|
99
|
-
-
|
|
100
|
-
metadata:
|
|
63
|
+
- GPL-3.0
|
|
64
|
+
metadata:
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
101
66
|
post_install_message:
|
|
102
67
|
rdoc_options: []
|
|
103
68
|
require_paths:
|
|
@@ -106,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
106
71
|
requirements:
|
|
107
72
|
- - ">="
|
|
108
73
|
- !ruby/object:Gem::Version
|
|
109
|
-
version:
|
|
74
|
+
version: 3.0.0
|
|
110
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
76
|
requirements:
|
|
112
77
|
- - ">="
|