yasuri 2.0.13 → 3.3.1
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 +5 -5
- data/.github/workflows/ruby.yml +35 -0
- data/.rubocop.yml +49 -0
- data/.rubocop_todo.yml +0 -0
- data/.ruby-version +1 -1
- data/README.md +82 -31
- data/Rakefile +1 -1
- data/USAGE.ja.md +366 -131
- data/USAGE.md +371 -136
- data/examples/example.rb +78 -0
- data/examples/github.yml +15 -0
- data/examples/sample.json +4 -0
- data/examples/sample.yml +11 -0
- data/exe/yasuri +5 -0
- data/lib/yasuri.rb +1 -0
- data/lib/yasuri/version.rb +1 -1
- data/lib/yasuri/yasuri.rb +96 -75
- data/lib/yasuri/yasuri_cli.rb +78 -0
- data/lib/yasuri/yasuri_links_node.rb +10 -6
- data/lib/yasuri/yasuri_map_node.rb +40 -0
- data/lib/yasuri/yasuri_node.rb +36 -4
- data/lib/yasuri/yasuri_node_generator.rb +17 -14
- data/lib/yasuri/yasuri_paginate_node.rb +26 -16
- data/lib/yasuri/yasuri_struct_node.rb +6 -4
- data/lib/yasuri/yasuri_text_node.rb +13 -8
- data/spec/cli_resources/tree.json +8 -0
- data/spec/cli_resources/tree.yml +5 -0
- data/spec/cli_resources/tree_wrong.json +9 -0
- data/spec/cli_resources/tree_wrong.yml +6 -0
- data/spec/servers/httpserver.rb +0 -2
- data/spec/spec_helper.rb +4 -11
- data/spec/yasuri_cli_spec.rb +114 -0
- data/spec/yasuri_links_node_spec.rb +92 -60
- data/spec/yasuri_map_spec.rb +71 -0
- data/spec/yasuri_paginate_node_spec.rb +99 -88
- data/spec/yasuri_spec.rb +196 -138
- data/spec/yasuri_struct_node_spec.rb +120 -100
- data/spec/yasuri_text_node_spec.rb +22 -32
- data/yasuri.gemspec +29 -22
- metadata +108 -19
- data/app.rb +0 -52
- data/spec/yasuri_node_spec.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0ec1d3a8cc766976c1d1329448a52df1696ccb50c41bf7c714b28cd265470d54
|
4
|
+
data.tar.gz: a87a1cd109c0e3dd8d820d3f35e34ef3096ef493d53d9c6bab0a79f1ccd7df9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f7787bfdc549e70b9e5a5ae45f84f3a0316142deea3c75f17d9781c8140e597d41ccbfd1679e8186799f76e771a24a1d6129402c818f5d6e3ea0b94128a2185
|
7
|
+
data.tar.gz: 61e430d7dae6fda7c28a9456f1751206cfcd01321ff71feeeba7447606a1eb10d7270dda5c8947a4cc8edb3dae60d6d57fc79f127e61920249fcab0ca47f4e37
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: Ruby
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ master ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ master ]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
test:
|
18
|
+
|
19
|
+
runs-on: ubuntu-latest
|
20
|
+
strategy:
|
21
|
+
matrix:
|
22
|
+
ruby-version: ['2.7', '3.0']
|
23
|
+
|
24
|
+
steps:
|
25
|
+
- uses: actions/checkout@v2
|
26
|
+
- name: Set up Ruby
|
27
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
28
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
29
|
+
# uses: ruby/setup-ruby@v1
|
30
|
+
uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
31
|
+
with:
|
32
|
+
ruby-version: ${{ matrix.ruby-version }}
|
33
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
34
|
+
- name: Run tests
|
35
|
+
run: bundle exec rake
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
# inherit_from: .rubocop_todo.yml
|
3
|
+
|
4
|
+
inherit_mode:
|
5
|
+
merge:
|
6
|
+
- Exclude
|
7
|
+
|
8
|
+
require:
|
9
|
+
- rubocop-performance
|
10
|
+
- rubocop-rspec
|
11
|
+
- rubocop-rubycw
|
12
|
+
|
13
|
+
AllCops:
|
14
|
+
DisabledByDefault: true
|
15
|
+
DisplayCopNames: true
|
16
|
+
Exclude:
|
17
|
+
- 'gems/**/*'
|
18
|
+
- 'pkg/**/*'
|
19
|
+
- 'coverage/**/*'
|
20
|
+
- 'exe/**/*'
|
21
|
+
|
22
|
+
NewCops: enable
|
23
|
+
|
24
|
+
Bundler:
|
25
|
+
Enabled: true
|
26
|
+
|
27
|
+
Gemspec:
|
28
|
+
Enabled: true
|
29
|
+
|
30
|
+
Lint:
|
31
|
+
Enabled: true
|
32
|
+
|
33
|
+
Performance:
|
34
|
+
Enabled: true
|
35
|
+
|
36
|
+
Rubycw:
|
37
|
+
Enabled: true
|
38
|
+
|
39
|
+
Security:
|
40
|
+
Enabled: true
|
41
|
+
|
42
|
+
Style/HashSyntax:
|
43
|
+
EnforcedStyle: ruby19
|
44
|
+
Style/HashEachMethods:
|
45
|
+
Enabled: true
|
46
|
+
Style/HashTransformKeys:
|
47
|
+
Enabled: true
|
48
|
+
Style/HashTransformValues:
|
49
|
+
Enabled: true
|
data/.rubocop_todo.yml
ADDED
File without changes
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.0
|
data/README.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
-
# Yasuri
|
1
|
+
# Yasuri
|
2
|
+
[](https://github.com/tac0x2a/yasuri/actions/workflows/ruby.yml)
|
3
|
+
[](https://coveralls.io/r/tac0x2a/yasuri?branch=master) [](https://codeclimate.com/github/tac0x2a/yasuri/maintainability)
|
2
4
|
|
3
|
-
Yasuri (鑢) is
|
5
|
+
Yasuri (鑢) is a library for declarative web scraping and a command line tool for scraping with it.
|
6
|
+
It performs scraping by simply describing the expected result in a simple declarative notation.
|
4
7
|
|
5
|
-
Yasuri
|
8
|
+
Yasuri makes it easy to write common scraping operations.
|
9
|
+
For example, the following processes can be easily implemented.
|
6
10
|
|
7
11
|
For example,
|
8
12
|
|
@@ -31,7 +35,10 @@ or
|
|
31
35
|
|
32
36
|
```ruby
|
33
37
|
# for Ruby 1.9.3 or lower
|
34
|
-
gem 'yasuri', '~>
|
38
|
+
gem 'yasuri', '~> 2.0', '>= 2.0.13'
|
39
|
+
|
40
|
+
# for Ruby 3.0.0 or lower
|
41
|
+
gem 'yasuri', '~> 3.1'
|
35
42
|
```
|
36
43
|
|
37
44
|
|
@@ -44,6 +51,7 @@ Or install it yourself as:
|
|
44
51
|
$ gem install yasuri
|
45
52
|
|
46
53
|
## Usage
|
54
|
+
### Use as library
|
47
55
|
|
48
56
|
```ruby
|
49
57
|
# Node tree constructing by DSL
|
@@ -55,46 +63,89 @@ root = Yasuri.links_root '//*[@id="menu"]/ul/li/a' do
|
|
55
63
|
|
56
64
|
# Node tree constructing by YAML
|
57
65
|
src = <<-EOYAML
|
58
|
-
|
59
|
-
node: links
|
66
|
+
links_root:
|
60
67
|
path: "//*[@id='menu']/ul/li/a"
|
61
|
-
|
62
|
-
|
63
|
-
node: text
|
64
|
-
path: "//*[@id='contents']/h2"
|
65
|
-
- content:
|
66
|
-
node: text
|
67
|
-
path: "//*[@id='contents']/p[1]"
|
68
|
+
text_title: "//*[@id='contents']/h2"
|
69
|
+
text_content: "//*[@id='contents']/p[1]"
|
68
70
|
EOYAML
|
69
71
|
root = Yasuri.yaml2tree(src)
|
70
72
|
|
71
73
|
|
72
74
|
# Node tree constructing by JSON
|
73
75
|
src = <<-EOJSON
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
},
|
82
|
-
{ "node" : "text",
|
83
|
-
"name" : "content",
|
84
|
-
"path" : "//*[@id='contents']/p[1]"
|
85
|
-
}
|
86
|
-
]
|
87
|
-
}
|
76
|
+
{
|
77
|
+
"links_root": {
|
78
|
+
"path": "//*[@id='menu']/ul/li/a",
|
79
|
+
"text_title": "//*[@id='contents']/h2",
|
80
|
+
"text_content": "//*[@id='contents']/p[1]"
|
81
|
+
}
|
82
|
+
}
|
88
83
|
EOJSON
|
89
84
|
root = Yasuri.json2tree(src)
|
90
85
|
|
91
|
-
|
92
|
-
|
86
|
+
# Execution and getting scraped result
|
87
|
+
result = root.scrape("http://some.scraping.page.tac42.net/")
|
88
|
+
# => [
|
89
|
+
# {"title" => "PageTitle 01", "content" => "Page Contents 01" },
|
90
|
+
# {"title" => "PageTitle 02", "content" => "Page Contents 02" },
|
91
|
+
# ...
|
92
|
+
# {"title" => "PageTitle N", "content" => "Page Contents N" }
|
93
|
+
# ]
|
94
|
+
```
|
95
|
+
|
96
|
+
### Use as CLI
|
97
|
+
|
98
|
+
```sh
|
99
|
+
# After gem installation..
|
100
|
+
$ yasuri help scrape
|
101
|
+
Usage:
|
102
|
+
yasuri scrape <URI> [[--file <TREE_FILE>] or [--json <JSON>]]
|
93
103
|
|
94
|
-
|
95
|
-
#
|
104
|
+
Options:
|
105
|
+
f, [--file=FILE] # path to file that written yasuri tree as json or yaml
|
106
|
+
j, [--json=JSON] # yasuri tree format json string
|
107
|
+
i, [--interval=N] # interval each request [ms]
|
108
|
+
|
109
|
+
Getting from <URI> and scrape it. with <JSON> or json/yml from <TREE_FILE>. They should be Yasuri's format json or yaml string.
|
96
110
|
```
|
97
111
|
|
112
|
+
Example
|
113
|
+
```sh
|
114
|
+
$ yasuri scrape "https://www.ruby-lang.org/en/" -j '
|
115
|
+
{
|
116
|
+
"text_title": "/html/head/title",
|
117
|
+
"text_desc": "//*[@id=\"intro\"]/p"
|
118
|
+
}'
|
119
|
+
|
120
|
+
{"title":"Ruby Programming Language","desc":"\n A dynamic, open source programming language with a focus on\n simplicity and productivity. It has an elegant syntax that is\n natural to read and easy to write.\n "}
|
121
|
+
```
|
122
|
+
|
123
|
+
## Dev
|
124
|
+
```sh
|
125
|
+
$ gem install bundler
|
126
|
+
$ bundle install
|
127
|
+
```
|
128
|
+
### Test
|
129
|
+
```sh
|
130
|
+
$ rake
|
131
|
+
# or
|
132
|
+
$ rspec spec/*spec.rb
|
133
|
+
```
|
134
|
+
|
135
|
+
### Test gem in local
|
136
|
+
```sh
|
137
|
+
$ gem build yasuri.gemspec
|
138
|
+
$ gem install yasuri-*.gem
|
139
|
+
```
|
140
|
+
### Release RubyGems
|
141
|
+
```sh
|
142
|
+
# Only first time
|
143
|
+
$ curl -u <user_name> https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials
|
144
|
+
$ chmod 0600 ~/.gem/credentials
|
145
|
+
|
146
|
+
$ nano lib/yasuri/version.rb # edit gem version
|
147
|
+
$ rake release
|
148
|
+
```
|
98
149
|
|
99
150
|
## Contributing
|
100
151
|
|
data/Rakefile
CHANGED
data/USAGE.ja.md
CHANGED
@@ -1,24 +1,32 @@
|
|
1
|
-
# Yasuri
|
1
|
+
# Yasuri
|
2
2
|
|
3
3
|
## Yasuri とは
|
4
|
-
Yasuri (鑢)
|
4
|
+
Yasuri (鑢) はWebスクレイピングを宣言的に行うためのライブラリと、それを用いたスクレイピングのコマンドラインツールです。
|
5
|
+
|
6
|
+
簡単な宣言的記法で期待結果を記述するだけでスクレイピングした結果を得られます。
|
5
7
|
|
6
8
|
Yasuriは、スクレイピングにおける、よくある処理を簡単に記述することができます.
|
7
|
-
|
9
|
+
例えば、以下のような処理を簡単に実現することができます.
|
8
10
|
|
9
|
-
+ ページ内の複数のリンクを開いて、各ページをスクレイピングした結果をHashで取得する
|
10
11
|
+ ページ内の複数のテキストをスクレイピングし、名前をつけてHashにする
|
12
|
+
+ ページ内の複数のリンクを開いて、各ページをスクレイピングした結果をHashで取得する
|
11
13
|
+ ページ内に繰り返し出現するテーブルをそれぞれスクレイピングして、配列として取得する
|
12
|
-
+
|
13
|
-
|
14
|
-
これらを簡単に実装することができます.
|
14
|
+
+ ページネーションで提供される各ページのうち、最初の3ページだけをスクレイピングする
|
15
15
|
|
16
16
|
## クイックスタート
|
17
17
|
|
18
|
+
#### インストール
|
19
|
+
```sh
|
20
|
+
# for Ruby 2.3.2
|
21
|
+
$ gem 'yasuri', '~> 2.0', '>= 2.0.13'
|
18
22
|
```
|
23
|
+
または
|
24
|
+
```sh
|
25
|
+
# for Ruby 3.0.0 or upper
|
19
26
|
$ gem install yasuri
|
20
27
|
```
|
21
28
|
|
29
|
+
#### ライブラリとして使う
|
22
30
|
```ruby
|
23
31
|
require 'yasuri'
|
24
32
|
require 'machinize'
|
@@ -29,96 +37,190 @@ root = Yasuri.links_root '//*[@id="menu"]/ul/li/a' do
|
|
29
37
|
text_content '//*[@id="contents"]/p[1]'
|
30
38
|
end
|
31
39
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
#
|
37
|
-
# {"title" => "
|
38
|
-
|
40
|
+
result = root.scrape("http://some.scraping.page.tac42.net/")
|
41
|
+
# => [
|
42
|
+
# {"title" => "PageTitle 01", "content" => "Page Contents 01" },
|
43
|
+
# {"title" => "PageTitle 02", "content" => "Page Contents 02" },
|
44
|
+
# ...
|
45
|
+
# {"title" => "PageTitle N", "content" => "Page Contents N" }
|
46
|
+
# ]
|
39
47
|
```
|
48
|
+
|
40
49
|
この例では、 LinkNode(`links_root`)の xpath で指定された各リンク先のページから、TextNode(`text_title`,`text_content`) の xpath で指定された2つのテキストをスクレイピングする例です.
|
41
50
|
|
42
51
|
(言い換えると、`//*[@id="menu"]/ul/li/a` で示される各リンクを開いて、`//*[@id="contents"]/h2` と `//*[@id="contents"]/p[1]` で指定されたテキストをスクレイピングします)
|
43
52
|
|
44
|
-
## 基本
|
45
53
|
|
46
|
-
|
47
|
-
|
54
|
+
#### CLIツールとして使う
|
55
|
+
上記と同じことを、CLIのコマンドとして実行できます。
|
56
|
+
|
57
|
+
```sh
|
58
|
+
$ yasuri scrape "http://some.scraping.page.tac42.net/" -j '
|
59
|
+
{
|
60
|
+
"links_root": {
|
61
|
+
"path": "//*[@id=\"menu\"]/ul/li/a",
|
62
|
+
"text_title": "//*[@id=\"contents\"]/h2",
|
63
|
+
"text_content": "//*[@id=\"contents\"]/p[1]"
|
64
|
+
}
|
65
|
+
}'
|
66
|
+
|
67
|
+
[
|
68
|
+
{"title":"PageTitle 01","content":"Page Contents 01"},
|
69
|
+
{"title":"PageTitle 02","content":"Page Contents 02"},
|
70
|
+
...,
|
71
|
+
{"title":"PageTitle N","content":"Page Contents N"}
|
72
|
+
]
|
73
|
+
```
|
48
74
|
|
75
|
+
結果はjson形式の文字列として取得できます。
|
49
76
|
|
50
|
-
|
77
|
+
----------------------------
|
78
|
+
## パースツリー
|
51
79
|
|
52
|
-
|
53
|
-
|
54
|
-
require 'yasuri'
|
80
|
+
パースツリーとは、スクレイピングする要素と出力構造を宣言的に定義するための木構造データです。
|
81
|
+
パースツリーは入れ子になった Node で構成されます.Node は `Type`, `Name`, `Path`, `Childlen`, `Options` 属性を持っており、その `Type` に応じたスクレイピング処理を行います.(ただし、`MapNode` のみ `Path` を持ちません)
|
55
82
|
|
56
83
|
|
57
|
-
|
58
|
-
tree = Yasuri.links_title '/html/body/a' do
|
59
|
-
text_name '/html/body/p'
|
60
|
-
end
|
61
|
-
|
62
|
-
# 2. Mechanize の agent と対象のページを与えてパースを開始する
|
63
|
-
agent = Mechanize.new
|
64
|
-
page = agent.get(uri)
|
84
|
+
パースツリーは以下のフォーマットで定義されます.
|
65
85
|
|
86
|
+
```ruby
|
87
|
+
# 1ノードからなる単純なツリー
|
88
|
+
Yasuri.<Type>_<Name> <Path> [,<Options>]
|
66
89
|
|
67
|
-
|
90
|
+
# 入れ子になっているツリー
|
91
|
+
Yasuri.<Type>_<Name> <Path> [,<Options>] do
|
92
|
+
<Type>_<Name> <Path> [,<Options>] do
|
93
|
+
<Type>_<Name> <Path> [,<Options>]
|
94
|
+
...
|
95
|
+
end
|
96
|
+
end
|
68
97
|
```
|
69
98
|
|
70
|
-
|
71
|
-
以下は、jsonで上記と等価な解析ツリーを定義した例です.
|
99
|
+
**例**
|
72
100
|
|
73
101
|
```ruby
|
74
|
-
#
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
]
|
85
|
-
}
|
86
|
-
EOJSON
|
87
|
-
tree = Yasuri.json2tree(src)
|
102
|
+
# 1ノードからなる単純なツリー
|
103
|
+
Yasuri.text_title '/html/head/title', truncate:/^[^,]+/
|
104
|
+
|
105
|
+
# 入れ子になっているツリー
|
106
|
+
Yasuri.links_root '//*[@id="menu"]/ul/li/a' do
|
107
|
+
struct_table './tr' do
|
108
|
+
text_title './td[1]'
|
109
|
+
text_pub_date './td[2]'
|
110
|
+
end
|
111
|
+
end
|
88
112
|
```
|
89
113
|
|
114
|
+
|
115
|
+
パースツリーはRubyのDSL、JSON、YAMLのいずれかで定義することができます。
|
116
|
+
以下は、上記と同じパースツリーをそれぞれの記法で定義した例です。
|
117
|
+
|
118
|
+
**Ruby DSLで定義する場合**
|
90
119
|
```ruby
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
120
|
+
Yasuri.links_title '/html/body/a' do
|
121
|
+
text_name '/html/body/p'
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
**JSONで定義する場合**
|
126
|
+
```json
|
127
|
+
{
|
128
|
+
links_title": {
|
129
|
+
"path": "/html/body/a",
|
130
|
+
"text_name": "/html/body/p"
|
131
|
+
}
|
132
|
+
}
|
133
|
+
```
|
134
|
+
|
135
|
+
**YAMLで定義する場合**
|
136
|
+
```yaml
|
137
|
+
links_title:
|
95
138
|
path: "/html/body/a"
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
139
|
+
text_name: "/html/body/p"
|
140
|
+
```
|
141
|
+
|
142
|
+
**パースツリーの特殊なケース**
|
143
|
+
|
144
|
+
rootの直下の要素が1つだけの場合、Hash(Object)ではなく、その要素を直接返します。
|
145
|
+
```json
|
146
|
+
{
|
147
|
+
"text_title": "/html/head/title",
|
148
|
+
"text_body": "/html/body",
|
149
|
+
}
|
150
|
+
# => {"title": "Welcome to yasuri!", "body": "Yasuri is ..."}
|
151
|
+
|
152
|
+
{
|
153
|
+
"text_title": "/html/head/title"}
|
154
|
+
}
|
155
|
+
# => Welcome to yasuri!
|
102
156
|
```
|
103
157
|
|
104
|
-
### Node
|
105
|
-
ツリーは入れ子になった *Node* で構成されます.
|
106
|
-
Node は `Type`, `Name`, `Path`, `Childlen`, `Options` を持っています.
|
107
158
|
|
108
|
-
Node
|
159
|
+
jsonまたはyaml形式では、子Nodeを持たない場合、`path` を直接値に指定することができます。以下の2つのjsonは同じパースツリーになります。
|
160
|
+
|
161
|
+
```json
|
162
|
+
{
|
163
|
+
"text_name": "/html/body/p"
|
164
|
+
}
|
109
165
|
|
166
|
+
{
|
167
|
+
"text_name": {
|
168
|
+
"path": "/html/body/p"
|
169
|
+
}
|
170
|
+
}
|
171
|
+
```
|
172
|
+
### ツリーを実行する
|
173
|
+
パースツリーのルートノードで`Node#scrape(uri, opt={})`メソッドをコールします。
|
174
|
+
|
175
|
+
**例**
|
110
176
|
```ruby
|
111
|
-
|
112
|
-
|
177
|
+
root = Yasuri.links_root '//*[@id="menu"]/ul/li/a' do
|
178
|
+
text_title '//*[@id="contents"]/h2'
|
179
|
+
text_content '//*[@id="contents"]/p[1]'
|
180
|
+
end
|
113
181
|
|
114
|
-
|
115
|
-
Yasuri.<Type>_<Name> <Path> [,<Options>] do
|
116
|
-
<Type>_<Name> <Path> [,<Options>] do
|
117
|
-
<Children>
|
118
|
-
end
|
119
|
-
end
|
182
|
+
result = root.scrape("http://some.scraping.page.tac42.net/", interval_ms: 1000)
|
120
183
|
```
|
121
184
|
|
185
|
+
+ `uri` はスクレイピングする対象ページのURIです。
|
186
|
+
+ `opt` はオプションをHashで指定します。以下のオプションを利用できます。
|
187
|
+
|
188
|
+
Yasuriはスクレイピングを行うエージェントとして、内部で`Mechanize`を使用しています。
|
189
|
+
このインスタンスを指定したい場合は、`Node#scrape_with_agent(uri, agent, opt={})`をコールします。
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
require 'logger'
|
193
|
+
|
194
|
+
agent = Mechanize.new
|
195
|
+
agent.log = Logger.new $stderr
|
196
|
+
agent.request_headers = {
|
197
|
+
# ...
|
198
|
+
}
|
199
|
+
|
200
|
+
result = root.scrape_with_agent(
|
201
|
+
"http://some.scraping.page.tac42.net/",
|
202
|
+
agent,
|
203
|
+
interval_ms: 1000)
|
204
|
+
```
|
205
|
+
|
206
|
+
### `opt`
|
207
|
+
#### `interval_ms`
|
208
|
+
複数ページにリクエストする際の間隔[ミリ秒]です。
|
209
|
+
|
210
|
+
省略した場合はインターバルなしで続けてリクエストしますが、多数のページへのリクエストが予想される場合、対象ホストが高負荷とならないよう、インターバル時間を指定することを強くお勧めします。
|
211
|
+
|
212
|
+
#### `retry_count`
|
213
|
+
ページ取得失敗時のリトライ回数です。省略した場合は5回リトライします。
|
214
|
+
|
215
|
+
#### `symbolize_names`
|
216
|
+
`true`のとき、結果セットのキーをシンボルとして返します。
|
217
|
+
|
218
|
+
--------------------------
|
219
|
+
## Node
|
220
|
+
|
221
|
+
Nodeはパースツリーの節または葉となる要素で、`Type`, `Name`, `Path`, `Childlen`, `Options` を持っており、その `Type` に応じてスクレイピングを行います.(ただし、`MapNode` のみ `Path` を持ちません)
|
222
|
+
|
223
|
+
|
122
224
|
#### Type
|
123
225
|
*Type* は Nodeの振る舞いを示します.Typeには以下のものがあります.
|
124
226
|
|
@@ -126,18 +228,21 @@ end
|
|
126
228
|
- *Struct*
|
127
229
|
- *Links*
|
128
230
|
- *Paginate*
|
231
|
+
- *Map*
|
129
232
|
|
130
|
-
|
233
|
+
詳細は各ノードの説明を参照してください。
|
234
|
+
|
235
|
+
#### Name
|
131
236
|
*Name* は 解析結果のHashにおけるキーになります.
|
132
237
|
|
133
|
-
|
238
|
+
#### Path
|
134
239
|
*Path* は xpath あるいは css セレクタによって、HTML上の特定のノードを指定します.
|
135
240
|
これは Machinize の `search` で使用されます.
|
136
241
|
|
137
|
-
|
242
|
+
#### Childlen
|
138
243
|
入れ子になっているノードの子ノードです.TextNodeはツリーの葉に当たるため、子ノードを持ちません.
|
139
244
|
|
140
|
-
|
245
|
+
#### Options
|
141
246
|
パースのオプションです.オプションはTypeごとに異なります.
|
142
247
|
各ノードに対して、`opt`メソッドをコールすることで、利用可能なオプションを取得できます.
|
143
248
|
|
@@ -153,7 +258,7 @@ node.opt #=> {:truncate => /^[^,]+/, :proc => nil}
|
|
153
258
|
### 例
|
154
259
|
|
155
260
|
```html
|
156
|
-
<!-- http://yasuri.example.net -->
|
261
|
+
<!-- http://yasuri.example.tac42.net -->
|
157
262
|
<html>
|
158
263
|
<head></head>
|
159
264
|
<body>
|
@@ -164,25 +269,24 @@ node.opt #=> {:truncate => /^[^,]+/, :proc => nil}
|
|
164
269
|
```
|
165
270
|
|
166
271
|
```ruby
|
167
|
-
agent = Mechanize.new
|
168
|
-
page = agent.get("http://yasuri.example.net")
|
169
|
-
|
170
272
|
p1 = Yasuri.text_title '/html/body/p[1]'
|
171
273
|
p1t = Yasuri.text_title '/html/body/p[1]', truncate:/^[^,]+/
|
172
|
-
p2u = Yasuri.text_title '/html/body/p[
|
274
|
+
p2u = Yasuri.text_title '/html/body/p[1]', proc: :upcase
|
173
275
|
|
174
|
-
p1.
|
175
|
-
p1t.
|
176
|
-
|
276
|
+
p1.scrape("http://yasuri.example.tac42.net") #=> "Hello,World"
|
277
|
+
p1t.scrape("http://yasuri.example.tac42.net") #=> "Hello"
|
278
|
+
p2u.scrape("http://yasuri.example.tac42.net") #=> "HELLO,WORLD"
|
177
279
|
```
|
178
280
|
|
281
|
+
なお、同じページ内の複数の要素を一度にスクレイピングする場合は、`MapNode`を使用します。詳細は、`MapNode`の例を参照してください。
|
282
|
+
|
179
283
|
### オプション
|
180
284
|
##### `truncate`
|
181
285
|
正規表現にマッチした文字列を取り出します.グループを指定した場合、最初にマッチしたグループだけを返します.
|
182
286
|
|
183
287
|
```ruby
|
184
288
|
node = Yasuri.text_example '/html/body/p[1]', truncate:/H(.+)i/
|
185
|
-
node.
|
289
|
+
node.scrape(uri)
|
186
290
|
#=> { "example" => "ello,Yasur" }
|
187
291
|
```
|
188
292
|
|
@@ -193,7 +297,7 @@ node.inject(agent, index_page)
|
|
193
297
|
|
194
298
|
```ruby
|
195
299
|
node = Yasuri.text_example '/html/body/p[1]', proc: :upcase, truncate:/H(.+)i/
|
196
|
-
node.
|
300
|
+
node.scrape(uri)
|
197
301
|
#=> { "example" => "ELLO,YASUR" }
|
198
302
|
```
|
199
303
|
|
@@ -208,7 +312,7 @@ Struct Node の `Path` が複数のタグにマッチする場合、配列とし
|
|
208
312
|
### 例
|
209
313
|
|
210
314
|
```html
|
211
|
-
<!-- http://yasuri.example.net -->
|
315
|
+
<!-- http://yasuri.example.tac42.net -->
|
212
316
|
<html>
|
213
317
|
<head>
|
214
318
|
<title>Books</title>
|
@@ -249,15 +353,12 @@ Struct Node の `Path` が複数のタグにマッチする場合、配列とし
|
|
249
353
|
```
|
250
354
|
|
251
355
|
```ruby
|
252
|
-
agent = Mechanize.new
|
253
|
-
page = agent.get("http://yasuri.example.net")
|
254
|
-
|
255
356
|
node = Yasuri.struct_table '/html/body/table[1]/tr' do
|
256
357
|
text_title './td[1]'
|
257
358
|
text_pub_date './td[2]'
|
258
|
-
|
359
|
+
end
|
259
360
|
|
260
|
-
node.
|
361
|
+
node.scrape("http://yasuri.example.tac42.net")
|
261
362
|
#=> [ { "title" => "The Perfect Insider",
|
262
363
|
# "pub_date" => "1996/4/5" },
|
263
364
|
# { "title" => "Doctors in Isolated Room",
|
@@ -271,23 +372,19 @@ Struct Node は xpath `'/html/body/table[1]/tr'` によって、最初の `<tabl
|
|
271
372
|
この場合は、最初の `<table>` は 3つの `<tr>`タグを持っているため、3つのHashを返します.(`<thead><tr>` は `Path` にマッチしないため4つではないことに注意)
|
272
373
|
各HashはTextNodeによってパースされたテキストを含んでいます.
|
273
374
|
|
274
|
-
|
275
375
|
また以下の例のように、Struct Node は TextNode以外のノードを子ノードとすることができます.
|
276
376
|
|
277
377
|
### 例
|
278
378
|
|
279
379
|
```ruby
|
280
|
-
agent = Mechanize.new
|
281
|
-
page = agent.get("http://yasuri.example.net")
|
282
|
-
|
283
380
|
node = Yasuri.strucre_tables '/html/body/table' do
|
284
381
|
struct_table './tr' do
|
285
382
|
text_title './td[1]'
|
286
383
|
text_pub_date './td[2]'
|
287
384
|
end
|
288
|
-
|
385
|
+
end
|
289
386
|
|
290
|
-
node.
|
387
|
+
node.scrape("http://yasuri.example.tac42.net")
|
291
388
|
|
292
389
|
#=> [ { "table" => [ { "title" => "The Perfect Insider",
|
293
390
|
# "pub_date" => "1996/4/5" },
|
@@ -319,8 +416,8 @@ node.inject(agent, page)
|
|
319
416
|
Links Node は リンクされた各ページをパースして結果を返します.
|
320
417
|
|
321
418
|
### 例
|
322
|
-
```
|
323
|
-
<!-- http://yasuri.example.net -->
|
419
|
+
```html
|
420
|
+
<!-- http://yasuri.example.tac42.net -->
|
324
421
|
<html>
|
325
422
|
<head><title>Yasuri Test</title></head>
|
326
423
|
<body>
|
@@ -332,8 +429,8 @@ Links Node は リンクされた各ページをパースして結果を返し
|
|
332
429
|
<title>
|
333
430
|
```
|
334
431
|
|
335
|
-
```
|
336
|
-
<!-- http://yasuri.example.net/child01.html -->
|
432
|
+
```html
|
433
|
+
<!-- http://yasuri.example.tac42.net/child01.html -->
|
337
434
|
<html>
|
338
435
|
<head><title>Child 01 Test</title></head>
|
339
436
|
<body>
|
@@ -346,8 +443,8 @@ Links Node は リンクされた各ページをパースして結果を返し
|
|
346
443
|
<title>
|
347
444
|
```
|
348
445
|
|
349
|
-
```
|
350
|
-
<!-- http://yasuri.example.net/child02.html -->
|
446
|
+
```html
|
447
|
+
<!-- http://yasuri.example.tac42.net/child02.html -->
|
351
448
|
<html>
|
352
449
|
<head><title>Child 02 Test</title></head>
|
353
450
|
<body>
|
@@ -356,8 +453,8 @@ Links Node は リンクされた各ページをパースして結果を返し
|
|
356
453
|
<title>
|
357
454
|
```
|
358
455
|
|
359
|
-
```
|
360
|
-
<!-- http://yasuri.example.net/child03.html -->
|
456
|
+
```html
|
457
|
+
<!-- http://yasuri.example.tac42.net/child03.html -->
|
361
458
|
<html>
|
362
459
|
<head><title>Child 03 Test</title></head>
|
363
460
|
<body>
|
@@ -369,22 +466,19 @@ Links Node は リンクされた各ページをパースして結果を返し
|
|
369
466
|
<title>
|
370
467
|
```
|
371
468
|
|
372
|
-
```
|
373
|
-
agent = Mechanize.new
|
374
|
-
page = agent.get("http://yasuri.example.net")
|
375
|
-
|
469
|
+
```ruby
|
376
470
|
node = Yasuri.links_title '/html/body/a' do
|
377
471
|
text_content '/html/body/p'
|
378
472
|
end
|
379
473
|
|
380
|
-
node.
|
474
|
+
node.scrape("http://yasuri.example.tac42.net")
|
381
475
|
#=> [ {"content" => "Child 01 page."},
|
382
476
|
{"content" => "Child 02 page."},
|
383
477
|
{"content" => "Child 03 page."}]
|
384
478
|
```
|
385
479
|
|
386
480
|
まず、 LinksNode は `Path` にマッチするすべてのリンクを最初のページから探します.
|
387
|
-
この例では、LinksNodeは `/html/body/a` にマッチするすべてのタグを `http://yasuri.example.net` から探します.
|
481
|
+
この例では、LinksNodeは `/html/body/a` にマッチするすべてのタグを `http://yasuri.example.tac42.net` から探します.
|
388
482
|
次に、見つかったタグのhref属性で指定されたページを開きます.(`./child01.html`, `./child02.html`, `./child03.html`)
|
389
483
|
|
390
484
|
開いた各ページに対して、子ノードによる解析を行います.LinksNodeは 各ページに対するパース結果をHashの配列として返します.
|
@@ -397,7 +491,7 @@ PaginateNodeは ページネーション(パジネーション, Pagination) で
|
|
397
491
|
`page02.html` から `page04.html` も同様です.
|
398
492
|
|
399
493
|
```html
|
400
|
-
<!-- http://yasuri.example.net/page01.html -->
|
494
|
+
<!-- http://yasuri.example.tac42.net/page01.html -->
|
401
495
|
<html>
|
402
496
|
<head><title>Page01</title></head>
|
403
497
|
<body>
|
@@ -417,17 +511,14 @@ PaginateNodeは ページネーション(パジネーション, Pagination) で
|
|
417
511
|
```
|
418
512
|
|
419
513
|
```ruby
|
420
|
-
agent = Mechanize.new
|
421
|
-
page = agent.get("http://yasuri.example.net/page01.html")
|
422
|
-
|
423
514
|
node = Yasuri.pages_root "/html/body/nav/span/a[@class='next']" , limit:3 do
|
424
515
|
text_content '/html/body/p'
|
425
516
|
end
|
426
517
|
|
427
|
-
node.
|
518
|
+
node.scrape("http://yasuri.example.tac42.net/page01.html")
|
428
519
|
#=> [ {"content" => "Patination01"},
|
429
|
-
|
430
|
-
|
520
|
+
# {"content" => "Patination02"},
|
521
|
+
# {"content" => "Patination03"}]
|
431
522
|
```
|
432
523
|
PaginateNodeは 次のページ を指すリンクを`Path`として指定する必要があります.
|
433
524
|
この例では、`NextPage` (`/html/body/nav/span/a[@class='next']`)が、次のページを指すリンクに該当します.
|
@@ -440,7 +531,7 @@ PaginateNodeは 次のページ を指すリンクを`Path`として指定する
|
|
440
531
|
node = Yasuri.pages_root "/html/body/nav/span/a[@class='next']" , limit:2 do
|
441
532
|
text_content '/html/body/p'
|
442
533
|
end
|
443
|
-
node.
|
534
|
+
node.scrape(uri)
|
444
535
|
#=> [ {"content" => "Pagination01"}, {"content" => "Pagination02"}]
|
445
536
|
```
|
446
537
|
この場合、PaginateNode は最大2つまでのページを開いてパースします.ページネーションは4つのページを持っているようですが、`limit:2`が指定されているため、結果の配列には2つの結果のみが含まれています.
|
@@ -449,33 +540,177 @@ node.inject(agent, page)
|
|
449
540
|
取得した各ページの結果を展開します.
|
450
541
|
|
451
542
|
```ruby
|
452
|
-
agent = Mechanize.new
|
453
|
-
page = agent.get("http://yasuri.example.net/page01.html")
|
454
|
-
|
455
543
|
node = Yasuri.pages_root "/html/body/nav/span/a[@class='next']" , flatten:true do
|
456
544
|
text_title '/html/head/title'
|
457
545
|
text_content '/html/body/p'
|
458
546
|
end
|
459
|
-
node.
|
547
|
+
node.scrape("http://yasuri.example.tac42.net/page01.html")
|
460
548
|
|
461
549
|
#=> [ {"title" => "Page01",
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
550
|
+
# "content" => "Patination01"},
|
551
|
+
# {"title" => "Page01",
|
552
|
+
# "content" => "Patination02"},
|
553
|
+
# {"title" => "Page01",
|
554
|
+
# "content" => "Patination03"}]
|
467
555
|
|
468
556
|
|
469
557
|
node = Yasuri.pages_root "/html/body/nav/span/a[@class='next']" , flatten:true do
|
470
558
|
text_title '/html/head/title'
|
471
559
|
text_content '/html/body/p'
|
472
560
|
end
|
473
|
-
node.
|
561
|
+
node.scrape("http://yasuri.example.tac42.net/page01.html")
|
474
562
|
|
475
563
|
#=> [ "Page01",
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
564
|
+
# "Patination01",
|
565
|
+
# "Page02",
|
566
|
+
# "Patination02",
|
567
|
+
# "Page03",
|
568
|
+
# "Patination03"]
|
569
|
+
```
|
570
|
+
|
571
|
+
## Map Node
|
572
|
+
*MapNode* はスクレイピングした結果をまとめるノードです.このノードはパースツリーにおいて常に節です.
|
573
|
+
|
574
|
+
### 例
|
575
|
+
|
576
|
+
```html
|
577
|
+
<!-- http://yasuri.example.tac42.net -->
|
578
|
+
<html>
|
579
|
+
<head><title>Yasuri Example</title></head>
|
580
|
+
<body>
|
581
|
+
<p>Hello,World</p>
|
582
|
+
<p>Hello,Yasuri</p>
|
583
|
+
</body>
|
584
|
+
</html>
|
585
|
+
```
|
586
|
+
|
587
|
+
```ruby
|
588
|
+
tree = Yasuri.map_root do
|
589
|
+
text_title '/html/head/title'
|
590
|
+
text_body_p '/html/body/p[1]'
|
591
|
+
end
|
592
|
+
|
593
|
+
tree.scrape("http://yasuri.example.tac42.net") #=> { "title" => "Yasuri Example", "body_p" => "Hello,World" }
|
594
|
+
|
595
|
+
|
596
|
+
tree = Yasuri.map_root do
|
597
|
+
map_group1 { text_child01 '/html/body/a[1]' }
|
598
|
+
map_group2 do
|
599
|
+
text_child01 '/html/body/a[1]'
|
600
|
+
text_child03 '/html/body/a[3]'
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
tree.scrape("http://yasuri.example.tac42.net") #=> {
|
605
|
+
# "group1" => {
|
606
|
+
# "child01" => "child01"
|
607
|
+
# },
|
608
|
+
# "group2" => {
|
609
|
+
# "child01" => "child01",
|
610
|
+
# "child03" => "child03"
|
611
|
+
# }
|
612
|
+
# }
|
613
|
+
```
|
614
|
+
|
615
|
+
### オプション
|
616
|
+
なし
|
617
|
+
|
618
|
+
|
619
|
+
-------------------------
|
620
|
+
## 使い方
|
621
|
+
|
622
|
+
### ライブラリとして使う
|
623
|
+
ライブラリとして使用する場合は、DSL, json, yaml の形式でツリーを定義できます。
|
624
|
+
|
625
|
+
```ruby
|
626
|
+
require 'yasuri'
|
627
|
+
|
628
|
+
# 1. パースツリーを作る
|
629
|
+
# DSLで定義する
|
630
|
+
tree = Yasuri.links_title '/html/body/a' do
|
631
|
+
text_name '/html/body/p'
|
632
|
+
end
|
633
|
+
|
634
|
+
# jsonで定義する場合
|
635
|
+
src = <<-EOJSON
|
636
|
+
{
|
637
|
+
links_title": {
|
638
|
+
"path": "/html/body/a",
|
639
|
+
"text_name": "/html/body/p"
|
640
|
+
}
|
641
|
+
}
|
642
|
+
EOJSON
|
643
|
+
tree = Yasuri.json2tree(src)
|
644
|
+
|
645
|
+
|
646
|
+
# yamlで定義する場合
|
647
|
+
src = <<-EOYAML
|
648
|
+
links_title:
|
649
|
+
path: "/html/body/a"
|
650
|
+
text_name: "/html/body/p"
|
651
|
+
EOYAML
|
652
|
+
tree = Yasuri.yaml2tree(src)
|
653
|
+
|
654
|
+
# 2. URLを与えてパースを開始する
|
655
|
+
tree.inject(uri)
|
656
|
+
```
|
657
|
+
|
658
|
+
### CLIツールとして使う
|
659
|
+
|
660
|
+
**ヘルプ表示**
|
661
|
+
```sh
|
662
|
+
$ yasuri help scrape
|
663
|
+
Usage:
|
664
|
+
yasuri scrape <URI> [[--file <TREE_FILE>] or [--json <JSON>]]
|
665
|
+
|
666
|
+
Options:
|
667
|
+
f, [--file=FILE] # path to file that written yasuri tree as json or yaml
|
668
|
+
j, [--json=JSON] # yasuri tree format json string
|
669
|
+
i, [--interval=N] # interval each request [ms]
|
670
|
+
|
671
|
+
Getting from <URI> and scrape it. with <JSON> or json/yml from <TREE_FILE>. They should be Yasuri's format json or yaml string.
|
672
|
+
```
|
673
|
+
|
674
|
+
CLIツールでは以下のどちらかの方法でパースツリーを指定します。
|
675
|
+
+ `--file`, `-f` : ファイルに出力されたjson形式またはyaml形式のパースツリーを読み込む
|
676
|
+
+ `--json`, `-j` : パースツリーを文字列として直接指定する
|
677
|
+
|
678
|
+
|
679
|
+
**パースツリーをファイルで指定する例**
|
680
|
+
```sh
|
681
|
+
% cat sample.yml
|
682
|
+
text_title: "/html/head/title"
|
683
|
+
text_desc: "//*[@id=\"intro\"]/p"
|
684
|
+
|
685
|
+
% yasuri scrape "https://www.ruby-lang.org/en/" --file sample.yml
|
686
|
+
{"title":"Ruby Programming Language","desc":"\n A dynamic, open source programming language with a focus on\n simplicity and productivity. It has an elegant syntax that is\n natural to read and easy to write.\n "}
|
687
|
+
|
688
|
+
% cat sample.json
|
689
|
+
{
|
690
|
+
"text_title": "/html/head/title",
|
691
|
+
"text_desc": "//*[@id=\"intro\"]/p"
|
692
|
+
}
|
693
|
+
|
694
|
+
% yasuri scrape "https://www.ruby-lang.org/en/" --file sample.json
|
695
|
+
{"title":"Ruby Programming Language","desc":"\n A dynamic, open source programming language with a focus on\n simplicity and productivity. It has an elegant syntax that is\n natural to read and easy to write.\n "}
|
481
696
|
```
|
697
|
+
|
698
|
+
ファイルがjsonまたはyamlのどちらで記載されているかについては自動判別されます。
|
699
|
+
|
700
|
+
**パースツリーをjsonで直接指定する例**
|
701
|
+
```sh
|
702
|
+
$ yasuri scrape "https://www.ruby-lang.org/en/" -j '
|
703
|
+
{
|
704
|
+
"text_title": "/html/head/title",
|
705
|
+
"text_desc": "//*[@id=\"intro\"]/p"
|
706
|
+
}'
|
707
|
+
|
708
|
+
{"title":"Ruby Programming Language","desc":"\n A dynamic, open source programming language with a focus on\n simplicity and productivity. It has an elegant syntax that is\n natural to read and easy to write.\n "}
|
709
|
+
```
|
710
|
+
|
711
|
+
#### その他のオプション
|
712
|
+
+ `--interval`, `-i` : 複数ページにリクエストする際の間隔[ミリ秒]です。
|
713
|
+
**例: 1秒間隔でリクエストする**
|
714
|
+
```sh
|
715
|
+
$ yasuri scrape "https://www.ruby-lang.org/en/" --file sample.yml --interval 1000
|
716
|
+
```
|