network-infra-utility 0.2.0 → 0.5.0
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/.gitignore +21 -0
- data/CHANGELOG.md +31 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +834 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
- data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
- data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
- data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
- data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
- data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
- data/network-infra-utility.gemspec +4 -2
- data/network.rb +3 -1
- data/service/geodb/GeoAPI.md +1 -0
- data/service/geodb/geodb.rb +278 -1
- data/service/ssh/README.md +942 -0
- data/service/ssh/bin/ssh-client +198 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
- data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
- data/service/ssh/ext/ssh_core/config/sys.config +0 -0
- data/service/ssh/ext/ssh_core/config/vm.args +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/rebar.config +24 -0
- data/service/ssh/ext/ssh_core/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
- data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
- data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
- data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
- data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +104 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
- data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
- data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
- data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
- data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
- data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
- data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
- data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
- data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
- data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
- data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
- data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
- data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
- data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
- data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
- data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
- data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
- data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
- data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
- data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
- data/support/basic/as_num.rb +221 -0
- data/support/basic/mac_address.rb +281 -0
- data/version.rb +1 -1
- metadata +138 -1
|
@@ -24,11 +24,13 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
# bin/ 既放对外命令 (geo-api) 也放开发脚本 (console/setup),
|
|
25
25
|
# executables 显式声明,避免把开发脚本当作系统命令安装到用户 PATH。
|
|
26
26
|
spec.bindir = "bin"
|
|
27
|
-
spec.executables = %w[geo-api geo-get geo-load]
|
|
28
|
-
spec.require_paths = ["document", "service", "support", "tool", "."]
|
|
27
|
+
spec.executables = %w[geo-api geo-get geo-load geo-doc]
|
|
28
|
+
spec.require_paths = ["document", "service", "service/ssh/lib", "support", "tool", "."]
|
|
29
29
|
|
|
30
30
|
# geo-api 命令行服务依赖的运行时 gem
|
|
31
31
|
spec.add_runtime_dependency "roda", "~> 3.0"
|
|
32
32
|
spec.add_runtime_dependency "rackup", "~> 2.0"
|
|
33
33
|
spec.add_runtime_dependency "puma", "~> 6.0"
|
|
34
|
+
spec.add_runtime_dependency "thor", "~> 1.3"
|
|
35
|
+
spec.add_runtime_dependency "json", "~> 2.6"
|
|
34
36
|
end
|
data/network.rb
CHANGED
|
@@ -13,12 +13,14 @@ require_relative "version"
|
|
|
13
13
|
module NetworkInfraUtility
|
|
14
14
|
# 支撑层:无外部依赖的基础工具,最先加载
|
|
15
15
|
require_relative "support/basic/ip"
|
|
16
|
+
require_relative "support/basic/as_num"
|
|
17
|
+
require_relative "support/basic/mac_address"
|
|
16
18
|
|
|
17
19
|
# 工具层:依赖 support
|
|
18
20
|
# require_relative "tool/xxx"
|
|
19
21
|
|
|
20
22
|
# 服务层:依赖 tool / support
|
|
21
|
-
|
|
23
|
+
require_relative "service/ssh/lib/network_infra_utility/ssh"
|
|
22
24
|
|
|
23
25
|
# 文档层:依赖 service / tool,最后加载
|
|
24
26
|
# require_relative "document/xxx"
|
data/service/geodb/GeoAPI.md
CHANGED
data/service/geodb/geodb.rb
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
#coding:utf-8
|
|
2
|
-
['cc','CasetDown/casetdown','network'].each{|mod|require mod}
|
|
2
|
+
['cc','CasetDown/casetdown','network','oj'].each{|mod|require mod}
|
|
3
3
|
CC.use 'file','enum','shell-tools'
|
|
4
4
|
|
|
5
5
|
# USAGE>
|
|
6
6
|
# GeoDB.load_asn("GeoLite2-ASN-CSV_*/*.csv")
|
|
7
7
|
# GeoDB.load_city("GeoLite2-City-CSV_*/*.csv")
|
|
8
8
|
# GeoDB.load_country("GeoLite2-Country-CSV_*/*.csv")
|
|
9
|
+
#
|
|
10
|
+
# GeoDB.output("geodb/")
|
|
11
|
+
# GeoDB.output("geodb/", format: :txt)
|
|
12
|
+
# GeoDB.output("geodb/", format: :csv)
|
|
13
|
+
# GeoDB.output("geodb/", output_path: "output/")
|
|
14
|
+
# GeoDB.output("geodb/", format: [:csv, :txt], output_path: "output/")
|
|
9
15
|
|
|
10
16
|
module GeoDB
|
|
11
17
|
module_function
|
|
12
18
|
|
|
19
|
+
# 加载 GeoLite2 ASN 数据(CSV → JSON)
|
|
20
|
+
# -----------------------------------------------------------
|
|
21
|
+
# 读取 GeoLite2-ASN-CSV_* 目录下的 CSV,将每条网段展开为
|
|
22
|
+
# [start_num, end_num] => record 的 Hash,序列化为 asn.json
|
|
23
|
+
# 供 geo-api 的 /geo/asn 接口做二分范围查找。
|
|
24
|
+
#
|
|
25
|
+
# 参数:
|
|
26
|
+
# dir_path — ASN CSV 的 glob 模式,默认当前目录查找
|
|
27
|
+
# out_path — JSON 输出目录,默认 geodb/
|
|
28
|
+
#
|
|
29
|
+
# 返回: asn Hash (range => record)
|
|
13
30
|
def load_asn dir_path="GeoLite2-ASN-CSV_*/*.csv", out_path=nil
|
|
14
31
|
out_path = "geodb/" unless out_path
|
|
15
32
|
Dir.mkdir(out_path) unless File.exist?(out_path)
|
|
@@ -29,6 +46,18 @@ module GeoDB
|
|
|
29
46
|
return asn
|
|
30
47
|
end
|
|
31
48
|
|
|
49
|
+
# 加载 GeoLite2 City 数据(CSV → JSON)
|
|
50
|
+
# -----------------------------------------------------------
|
|
51
|
+
# 分两阶段处理 City 数据:
|
|
52
|
+
# 1. City-Locations-zh-CN → geo-city.json (geoname_id => 城市地理信息)
|
|
53
|
+
# 2. City-Blocks-IPv4/IPv6 → city-IPv4.json / city-IPv6.json
|
|
54
|
+
# (网段 [start, end] => record, 含经纬度/精度半径等)
|
|
55
|
+
#
|
|
56
|
+
# 参数:
|
|
57
|
+
# dir_path — City CSV 的 glob 模式,默认当前目录查找
|
|
58
|
+
# out_path — JSON 输出目录,默认 geodb/
|
|
59
|
+
#
|
|
60
|
+
# 返回: [city, geo] 两个 Hash
|
|
32
61
|
def load_city dir_path="GeoLite2-City-CSV_*/*.csv", out_path=nil
|
|
33
62
|
out_path = "geodb/" unless out_path
|
|
34
63
|
Dir.mkdir(out_path) unless File.exist?(out_path)
|
|
@@ -62,6 +91,18 @@ module GeoDB
|
|
|
62
91
|
return city,geo
|
|
63
92
|
end
|
|
64
93
|
|
|
94
|
+
# 加载 GeoLite2 Country 数据(CSV → JSON)
|
|
95
|
+
# -----------------------------------------------------------
|
|
96
|
+
# 分两阶段处理 Country 数据:
|
|
97
|
+
# 1. Country-Locations-zh-CN → geo-country.json (geoname_id => 国家信息)
|
|
98
|
+
# 2. Country-Blocks-IPv4/IPv6 → country-IPv4.json / country-IPv6.json
|
|
99
|
+
# (网段 [start, end] => record)
|
|
100
|
+
#
|
|
101
|
+
# 参数:
|
|
102
|
+
# dir_path — Country CSV 的 glob 模式,默认当前目录查找
|
|
103
|
+
# out_path — JSON 输出目录,默认 geodb/
|
|
104
|
+
#
|
|
105
|
+
# 返回: [country, geo] 两个 Hash
|
|
65
106
|
def load_country dir_path="GeoLite2-Country-CSV_*/*.csv", out_path=nil
|
|
66
107
|
out_path = "geodb/" unless out_path
|
|
67
108
|
Dir.mkdir(out_path) unless File.exist?(out_path)
|
|
@@ -94,4 +135,240 @@ module GeoDB
|
|
|
94
135
|
end
|
|
95
136
|
return country,geo
|
|
96
137
|
end
|
|
138
|
+
|
|
139
|
+
# ============================================================
|
|
140
|
+
# output — 将已导入的 JSON 拼接成一张大表并输出文件
|
|
141
|
+
# ------------------------------------------------------------
|
|
142
|
+
# 对 data_path 下的 city-IPv4/IPv6、country-IPv4/IPv6 逐条关联
|
|
143
|
+
# asn 和 geo-city / geo-country,以 network 为联合主键合并,
|
|
144
|
+
# 最终按 IPv4 / IPv6 分别输出为大表文件。
|
|
145
|
+
#
|
|
146
|
+
# 参数:
|
|
147
|
+
# data_path — 数据目录路径(末尾可带可不带分隔符)
|
|
148
|
+
# format — 输出格式,:csv / :txt / [:csv, :txt](默认两者都输出)
|
|
149
|
+
# output_path — 输出目录路径,默认 geodb/(与数据目录相同)
|
|
150
|
+
#
|
|
151
|
+
# CSV: 符合 RFC 4180,对含逗号/引号/换行的字段自动加引号转义
|
|
152
|
+
# TXT: 以 \n 换行、\t 分隔单元格,空值留空
|
|
153
|
+
#
|
|
154
|
+
# 用法:
|
|
155
|
+
# GeoDB.output("geodb/")
|
|
156
|
+
# GeoDB.output("geodb/", format: :csv)
|
|
157
|
+
# GeoDB.output("geodb/", output_path: "output/")
|
|
158
|
+
# ============================================================
|
|
159
|
+
|
|
160
|
+
# output 表头
|
|
161
|
+
OUTPUT_HEADERS = %w[
|
|
162
|
+
network start_num end_num
|
|
163
|
+
asn_number asn_organization
|
|
164
|
+
city_geoname_id city_latitude city_longitude city_accuracy_radius
|
|
165
|
+
continent_code continent_name country_iso_code country_name
|
|
166
|
+
subdivision_1_name subdivision_2_name city_name time_zone
|
|
167
|
+
registered_country_geoname_id registered_country_name
|
|
168
|
+
is_anonymous_proxy is_satellite_provider is_anycast
|
|
169
|
+
].freeze
|
|
170
|
+
|
|
171
|
+
# IPv4 数值上界(不含),用于区分 v4 / v6
|
|
172
|
+
IPv4_MAX = 4294967296 # 2**32
|
|
173
|
+
|
|
174
|
+
def output data_path="geodb/", format: [:csv, :txt], output_path: nil
|
|
175
|
+
data_path = data_path.chomp(File::SEPARATOR) + File::SEPARATOR
|
|
176
|
+
# 输出目录: 指定了就用指定的,没指定则与数据目录相同
|
|
177
|
+
if output_path
|
|
178
|
+
out_dir = output_path.chomp(File::SEPARATOR) + File::SEPARATOR
|
|
179
|
+
Dir.mkdir(out_dir) unless File.exist?(out_dir)
|
|
180
|
+
else
|
|
181
|
+
out_dir = data_path
|
|
182
|
+
end
|
|
183
|
+
formats = format.is_a?(Array) ? format : [format]
|
|
184
|
+
|
|
185
|
+
# ---- 1. 加载 asn,排序后用于二分查找 ----
|
|
186
|
+
puts "[output] 加载 asn.json ..."
|
|
187
|
+
asn_arr = []
|
|
188
|
+
_stream_entries("#{data_path}asn.json") do |key, record|
|
|
189
|
+
start_num, end_num = key.scan(/\d+/).map(&:to_i)
|
|
190
|
+
asn_arr << [start_num, end_num, record]
|
|
191
|
+
end
|
|
192
|
+
asn_arr.sort_by! { |s, _, _| s }
|
|
193
|
+
puts "[output] asn 排序完成,共 #{asn_arr.size} 条"
|
|
194
|
+
|
|
195
|
+
# ---- 2. 加载 geo-city / geo-country(整表载入,文件不大)----
|
|
196
|
+
puts "[output] 加载 geo-city.json ..."
|
|
197
|
+
geo_city = Oj.load_file("#{data_path}geo-city.json")
|
|
198
|
+
puts "[output] geo-city: #{geo_city.size} 条"
|
|
199
|
+
puts "[output] 加载 geo-country.json ..."
|
|
200
|
+
geo_country = Oj.load_file("#{data_path}geo-country.json")
|
|
201
|
+
puts "[output] geo-country: #{geo_country.size} 条"
|
|
202
|
+
|
|
203
|
+
# ---- 3. 分别处理 IPv4 / IPv6 ----
|
|
204
|
+
{ v4: 'IPv4', v6: 'IPv6' }.each do |tag, label|
|
|
205
|
+
puts "\n[output] === 处理 #{label} ==="
|
|
206
|
+
|
|
207
|
+
# 收集所有 city / country 记录到内存 Hash
|
|
208
|
+
# key=[start_num, end_num] => { city: record, country: record }
|
|
209
|
+
# city 和 country 的 geoname_id 同名但语义不同(城市级 vs 国家级),
|
|
210
|
+
# 不能直接 merge!,需分别存放各自关联各自的 geo 表。
|
|
211
|
+
records = {}
|
|
212
|
+
|
|
213
|
+
["city-#{label}", "country-#{label}"].each do |file|
|
|
214
|
+
source = file.start_with?('city') ? :city : :country
|
|
215
|
+
path = "#{data_path}#{file}.json"
|
|
216
|
+
next unless File.exist?(path)
|
|
217
|
+
count = 0
|
|
218
|
+
_stream_entries(path) do |key, rec|
|
|
219
|
+
start_num, end_num = key.scan(/\d+/).map(&:to_i)
|
|
220
|
+
# v6 文件可能混入 v4 数据(load 阶段历史问题),按 start_num 区分
|
|
221
|
+
is_v6_data = start_num >= IPv4_MAX
|
|
222
|
+
next if tag == :v4 && is_v6_data
|
|
223
|
+
next if tag == :v6 && !is_v6_data
|
|
224
|
+
|
|
225
|
+
k = [start_num, end_num]
|
|
226
|
+
records[k] ||= {}
|
|
227
|
+
records[k][source] = rec
|
|
228
|
+
count += 1
|
|
229
|
+
end
|
|
230
|
+
puts "[output] #{file}.json → #{count} 条"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
next if records.empty?
|
|
234
|
+
puts "[output] 合并后 #{records.size} 个唯一网段"
|
|
235
|
+
|
|
236
|
+
# 排序
|
|
237
|
+
sorted_keys = records.keys.sort_by { |s, _| s }
|
|
238
|
+
|
|
239
|
+
# 输出
|
|
240
|
+
formats.each do |fmt|
|
|
241
|
+
ext = fmt == :csv ? 'csv' : 'txt'
|
|
242
|
+
out_file = "#{out_dir}output-#{label.downcase}.#{ext}"
|
|
243
|
+
total = sorted_keys.size
|
|
244
|
+
puts "[output] 写入 #{out_file} (#{total} 行, #{fmt}) ..."
|
|
245
|
+
|
|
246
|
+
if fmt == :csv
|
|
247
|
+
_write_csv(out_file, sorted_keys, records, asn_arr, geo_city, geo_country)
|
|
248
|
+
else
|
|
249
|
+
_write_txt(out_file, sorted_keys, records, asn_arr, geo_city, geo_country)
|
|
250
|
+
end
|
|
251
|
+
puts "[output] ✓ #{out_file} 完成"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# ---- output 内部方法 ----
|
|
257
|
+
|
|
258
|
+
# Oj ScHandler 流式收集器:逐条回调 key-value
|
|
259
|
+
class RangeCollector < Oj::ScHandler
|
|
260
|
+
def initialize(&block)
|
|
261
|
+
@block = block
|
|
262
|
+
@depth = 0
|
|
263
|
+
end
|
|
264
|
+
def hash_start; @depth += 1; {}; end
|
|
265
|
+
def hash_end; @depth -= 1; end
|
|
266
|
+
def array_start; []; end
|
|
267
|
+
def array_end; end
|
|
268
|
+
def hash_set(hash, key, value)
|
|
269
|
+
if @depth == 1
|
|
270
|
+
@block.call(key, value)
|
|
271
|
+
else
|
|
272
|
+
hash[key] = value
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
def array_append(array, value)
|
|
276
|
+
array << value
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# 流式遍历 JSON 文件,每条 entry 回调 yield(key, record)
|
|
281
|
+
def _stream_entries path
|
|
282
|
+
return unless File.exist?(path)
|
|
283
|
+
collector = RangeCollector.new { |k, v| yield k, v }
|
|
284
|
+
File.open(path, 'r') { |f| Oj.sc_parse(collector, f) }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# 二分查找:在排序的 [[start, end, record], ...] 中找包含 num 的范围
|
|
288
|
+
def _find_range arr, num
|
|
289
|
+
return nil if arr.nil? || arr.empty?
|
|
290
|
+
idx = arr.bsearch_index { |s, _, _| s > num }
|
|
291
|
+
cand = idx.nil? ? arr.last : (idx > 0 ? arr[idx - 1] : nil)
|
|
292
|
+
return nil unless cand
|
|
293
|
+
s, e, r = cand
|
|
294
|
+
(num >= s && num <= e) ? r : nil
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# 为一条记录构建完整输出行
|
|
298
|
+
# entry 是 { city: record_hash, country: record_hash } 结构(任一可缺)
|
|
299
|
+
def _build_row start_num, end_num, entry, asn_arr, geo_city, geo_country
|
|
300
|
+
city_rec = entry[:city]
|
|
301
|
+
country_rec = entry[:country]
|
|
302
|
+
# network 优先从 city 取(有更丰富字段),回退到 country
|
|
303
|
+
network = (city_rec || country_rec)['network']
|
|
304
|
+
|
|
305
|
+
# ASN 关联(用 start_num 查)
|
|
306
|
+
asn_rec = _find_range(asn_arr, start_num)
|
|
307
|
+
|
|
308
|
+
# geo-city 关联(用 city 表的 geoname_id)
|
|
309
|
+
city_gid = city_rec ? city_rec['geoname_id'] : nil
|
|
310
|
+
city_geo = (city_gid && city_rec.key?('latitude')) ? geo_city[city_gid] : nil
|
|
311
|
+
|
|
312
|
+
# geo-country 关联(用 country 表的 geoname_id)
|
|
313
|
+
country_gid = country_rec ? country_rec['geoname_id'] : nil
|
|
314
|
+
country_geo = country_gid ? geo_country[country_gid] : nil
|
|
315
|
+
|
|
316
|
+
# 用于 continent / country 的统一取值(优先 city_geo,其次 country_geo)
|
|
317
|
+
unified_geo = city_geo || country_geo
|
|
318
|
+
|
|
319
|
+
# registered_country(city 和 country 表都有该字段,优先 city)
|
|
320
|
+
reg_id = (city_rec || country_rec)['registered_country_geoname_id']
|
|
321
|
+
reg_country_geo = reg_id ? geo_country[reg_id] : nil
|
|
322
|
+
|
|
323
|
+
OUTPUT_HEADERS.map do |h|
|
|
324
|
+
case h
|
|
325
|
+
when 'network' then network
|
|
326
|
+
when 'start_num' then start_num
|
|
327
|
+
when 'end_num' then end_num
|
|
328
|
+
when 'asn_number' then asn_rec ? asn_rec['autonomous_system_number'] : nil
|
|
329
|
+
when 'asn_organization' then asn_rec ? asn_rec['autonomous_system_organization'] : nil
|
|
330
|
+
when 'city_geoname_id' then city_gid
|
|
331
|
+
when 'city_latitude' then city_rec ? city_rec['latitude'] : nil
|
|
332
|
+
when 'city_longitude' then city_rec ? city_rec['longitude'] : nil
|
|
333
|
+
when 'city_accuracy_radius' then city_rec ? city_rec['accuracy_radius'] : nil
|
|
334
|
+
when 'continent_code' then unified_geo ? unified_geo['continent_code'] : nil
|
|
335
|
+
when 'continent_name' then unified_geo ? unified_geo['continent_name'] : nil
|
|
336
|
+
when 'country_iso_code' then unified_geo ? unified_geo['country_iso_code'] : nil
|
|
337
|
+
when 'country_name' then unified_geo ? unified_geo['country_name'] : nil
|
|
338
|
+
when 'subdivision_1_name' then city_geo ? city_geo['subdivision_1_name'] : nil
|
|
339
|
+
when 'subdivision_2_name' then city_geo ? city_geo['subdivision_2_name'] : nil
|
|
340
|
+
when 'city_name' then city_geo ? city_geo['city_name'] : nil
|
|
341
|
+
when 'time_zone' then city_geo ? city_geo['time_zone'] : nil
|
|
342
|
+
when 'registered_country_geoname_id' then reg_id
|
|
343
|
+
when 'registered_country_name' then reg_country_geo ? reg_country_geo['country_name'] : nil
|
|
344
|
+
when 'is_anonymous_proxy' then (city_rec || country_rec)['is_anonymous_proxy']
|
|
345
|
+
when 'is_satellite_provider' then (city_rec || country_rec)['is_satellite_provider']
|
|
346
|
+
when 'is_anycast' then (city_rec || country_rec)['is_anycast']
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# 写 CSV 文件(自动对复杂内容加引号)
|
|
352
|
+
def _write_csv out_file, sorted_keys, records, asn_arr, geo_city, geo_country
|
|
353
|
+
CSV.open(out_file, 'w', encoding: 'utf-8') do |csv|
|
|
354
|
+
csv << OUTPUT_HEADERS
|
|
355
|
+
sorted_keys.each do |start_num, end_num|
|
|
356
|
+
row = _build_row(start_num, end_num, records[[start_num, end_num]],
|
|
357
|
+
asn_arr, geo_city, geo_country)
|
|
358
|
+
csv << row
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# 写 TXT 文件(\n 换行, \t 分隔)
|
|
364
|
+
def _write_txt out_file, sorted_keys, records, asn_arr, geo_city, geo_country
|
|
365
|
+
File.open(out_file, 'w:UTF-8') do |f|
|
|
366
|
+
f.puts OUTPUT_HEADERS.join("\t")
|
|
367
|
+
sorted_keys.each do |start_num, end_num|
|
|
368
|
+
row = _build_row(start_num, end_num, records[[start_num, end_num]],
|
|
369
|
+
asn_arr, geo_city, geo_country)
|
|
370
|
+
f.puts row.map { |v| v.nil? ? '' : v.to_s }.join("\t")
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
97
374
|
end
|