web_crawler 0.3.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README +22 -1
- data/lib/web_crawler.rb +2 -0
- data/lib/web_crawler/application.rb +33 -2
- data/lib/web_crawler/base.rb +113 -0
- data/lib/web_crawler/batch_request.rb +10 -4
- data/lib/web_crawler/cached_request.rb +16 -7
- data/lib/web_crawler/configuration.rb +5 -5
- data/lib/web_crawler/factory_url.rb +27 -7
- data/lib/web_crawler/follower.rb +11 -9
- data/lib/web_crawler/parsers.rb +1 -0
- data/lib/web_crawler/parsers/mapper.rb +114 -0
- data/lib/web_crawler/parsers/url.rb +3 -5
- data/lib/web_crawler/request.rb +14 -2
- data/lib/web_crawler/response.rb +2 -2
- data/lib/web_crawler/version.rb +2 -2
- data/lib/web_crawler/view.rb +1 -1
- data/lib/web_crawler/view/csv.rb +1 -1
- data/lib/web_crawler/view/json.rb +1 -1
- data/lib/web_crawler/view/yaml.rb +1 -1
- data/spec/fixtures/example.xml +171 -0
- data/spec/fixtures/my_crawler.rb +82 -0
- data/spec/fixtures/test_crawler.rb +108 -0
- data/spec/fixtures/test_crawler2.rb +77 -0
- data/spec/spec_helper.rb +8 -3
- data/spec/web_crawler/batch_request_spec.rb +0 -11
- data/spec/web_crawler/cached_request_spec.rb +17 -11
- data/spec/web_crawler/factory_url_spec.rb +19 -6
- data/spec/web_crawler/follow_spec.rb +11 -4
- data/spec/web_crawler/view_spec.rb +10 -10
- data/spec/web_crawler/web_crawler_api_base_class_spec.rb +143 -0
- data/web_crawler.gemspec +2 -0
- metadata +43 -8
@@ -0,0 +1,114 @@
|
|
1
|
+
class WebCrawler::Parsers::Mapper
|
2
|
+
|
3
|
+
class Filter
|
4
|
+
def initialize(method, context)
|
5
|
+
@method, @context = method, context
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(*args, &blk)
|
9
|
+
return @context.send(@method, *args, &blk) if @method.is_a? Symbol
|
10
|
+
return @method.call(*args, &blk) if @method.respond_to? :call
|
11
|
+
return args.size == 1 ? args.first : args if @method.nil?
|
12
|
+
raise ArgumentError, "#{@method} must be a Symbol or Object which respond with in :call or nil"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Map
|
17
|
+
cattr_accessor :default_options
|
18
|
+
self.default_options = { :on => :inner_text }
|
19
|
+
|
20
|
+
def initialize(selector, binding, options, &block)
|
21
|
+
@selector, @options = selector, self.class.default_options.merge(options)
|
22
|
+
@binding = binding
|
23
|
+
@block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_block?
|
27
|
+
@block.present?
|
28
|
+
end
|
29
|
+
|
30
|
+
def element
|
31
|
+
@selector
|
32
|
+
end
|
33
|
+
|
34
|
+
def in(context)
|
35
|
+
context.search(element).each do |el|
|
36
|
+
yield el, @block.call
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def on
|
41
|
+
@options[:on]
|
42
|
+
end
|
43
|
+
|
44
|
+
def to(context)
|
45
|
+
@options[:to].respond_to?(:call) ? @options[:to].call(context) : @options[:to]
|
46
|
+
end
|
47
|
+
|
48
|
+
def filter
|
49
|
+
@filter ||= Filter.new(@options[:filter], @binding)
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(context)
|
53
|
+
value = context.search(element)
|
54
|
+
value = value.send(*on) if on.present?
|
55
|
+
filter.call(value)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :selector, :element, :mapping, :klass, :name
|
60
|
+
|
61
|
+
def initialize(name, binding, selector)
|
62
|
+
@name = name
|
63
|
+
@binding = binding
|
64
|
+
@selector = selector
|
65
|
+
@mapping = { }
|
66
|
+
end
|
67
|
+
|
68
|
+
def callback(&block)
|
69
|
+
if block_given?
|
70
|
+
@callback = block
|
71
|
+
else
|
72
|
+
@callback
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_map(selector, options = { })
|
77
|
+
Map.new(selector, @binding, options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def map(selector, options = { }, &block)
|
81
|
+
@mapping[selector] = Map.new(selector, @binding, options, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def collect(response)
|
85
|
+
doc = Hpricot(response.to_s, :xml => response.xml?)
|
86
|
+
[].tap do |collected|
|
87
|
+
doc.search(selector).each do |context|
|
88
|
+
collected << { }
|
89
|
+
collect_with_mapping(context, collected) unless mapping.empty?
|
90
|
+
collect_with_callback(context, collected) if callback
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
def collect_with_mapping(context, collected)
|
98
|
+
mapping.each_value do |map|
|
99
|
+
if map.with_block?
|
100
|
+
map.in(context) do |element, sub_map|
|
101
|
+
collected.last[sub_map.to(element)] = sub_map.call(element)
|
102
|
+
end
|
103
|
+
else
|
104
|
+
collected.last[map.to(context)] = map.call(context)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def collect_with_callback(context, collected)
|
110
|
+
callback.call(context).each do |key, value|
|
111
|
+
collected.last[key] = value
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -11,11 +11,9 @@ class WebCrawler::Parsers::Url
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def parse(response, &filter)
|
14
|
-
(Hpricot(response.to_s) / "a").map
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
end.compact.uniq
|
14
|
+
(Hpricot(response.to_s) / "a").map { |a| normalize(a["href"]) }.compact.uniq.tap do |result|
|
15
|
+
result = result.select(&filter) if block_given?
|
16
|
+
end
|
19
17
|
end
|
20
18
|
|
21
19
|
def normalize(url)
|
data/lib/web_crawler/request.rb
CHANGED
@@ -12,13 +12,23 @@ module WebCrawler
|
|
12
12
|
|
13
13
|
attr_reader :url, :response
|
14
14
|
|
15
|
-
def initialize(url)
|
15
|
+
def initialize(url, custom_headers = { })
|
16
16
|
@url, @request = normalize_url(url), { }
|
17
|
-
@headers = HEADERS.dup
|
17
|
+
@headers = HEADERS.dup.merge(custom_headers)
|
18
|
+
@ready = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def ready?
|
22
|
+
@ready
|
18
23
|
end
|
19
24
|
|
20
25
|
def process
|
21
26
|
@response = Response.new *fetch(url)
|
27
|
+
@ready = true
|
28
|
+
response
|
29
|
+
rescue Errno::ECONNREFUSED => e
|
30
|
+
WebCrawler.logger.error "request to #{url} failed: #{e.message}"
|
31
|
+
return nil
|
22
32
|
end
|
23
33
|
|
24
34
|
def inspect
|
@@ -39,7 +49,9 @@ module WebCrawler
|
|
39
49
|
|
40
50
|
def fetch(uri, limit = 3, redirect_path = nil)
|
41
51
|
raise ArgumentError, "HTTP redirect too deep. #{redirected_from} => #{uri}" if limit <= 0
|
52
|
+
|
42
53
|
response = request_for(uri.host, uri.port).get(uri.request_uri, headers)
|
54
|
+
|
43
55
|
case response
|
44
56
|
when Net::HTTPRedirection then
|
45
57
|
@headers['Cookie'] = response['Set-Cookie'] if response['Set-Cookie']
|
data/lib/web_crawler/response.rb
CHANGED
@@ -47,7 +47,7 @@ module WebCrawler
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def mime_type
|
50
|
-
MIME::Types[header['content-type']].first
|
50
|
+
MIME::Types[header['content-type'] || "text/html; charset=utf-8"].first
|
51
51
|
end
|
52
52
|
|
53
53
|
def header
|
@@ -55,7 +55,7 @@ module WebCrawler
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def body
|
58
|
-
|
58
|
+
encoding = (self['Content-Type'] || 'text/html; charset=UTF-8').split("=").last
|
59
59
|
@body ||= if encoding.upcase == 'UTF-8'
|
60
60
|
@response.body
|
61
61
|
else
|
data/lib/web_crawler/version.rb
CHANGED
data/lib/web_crawler/view.rb
CHANGED
data/lib/web_crawler/view/csv.rb
CHANGED
@@ -0,0 +1,171 @@
|
|
1
|
+
<?xml version="1.0" encoding="windows-1251"?>
|
2
|
+
<jobs count="5">
|
3
|
+
<job id="11477318">
|
4
|
+
<link>http://nsk.superjob.ru/vacancy/?id=11477318</link>
|
5
|
+
<name>�����������</name>
|
6
|
+
<region>�����������</region>
|
7
|
+
<salary>�� 10000 ���. � �����</salary>
|
8
|
+
<description>����������� �����������:
|
9
|
+
������������ � ����������� ������-�������������� ������������, �������������� ����������� �������� � ������� ������� �����;
|
10
|
+
���������� ������ �� �������� � ����� �������� �������, �������� �� �����, ����������� �����, ������, ������;
|
11
|
+
�������� �� ���������� � ����������� �������� ������� �������,
|
12
|
+
���������� �� ������, ���������� ��������� � ���������� �� ������� ��� ����������� � ���������� � ���������� ��������;
|
13
|
+
�������� ����� �������� ������������� ���������, �������������� ������������ � ���������������� �������;
|
14
|
+
������ ��������� �� �������� ���������� ����� � �����;
|
15
|
+
����������� ������� �����������.
|
16
|
+
|
17
|
+
���������� � ������������:
|
18
|
+
���������-�������������� ����������� (������ ���������) �����������. ����������������, ���������������, ������������������, ����������, ����������������, �������� ���������, ������������. ������ �������� � ������.
|
19
|
+
|
20
|
+
������� ������ � �����������:
|
21
|
+
� ������������ ����� ��������� ����������� �� ����� ������ �������.
|
22
|
+
���������� �����, 10000 ������ �� 14 ����. ����� � 15.06.2011�.
|
23
|
+
</description>
|
24
|
+
<company id="636589">
|
25
|
+
�������� �. �.
|
26
|
+
</company>
|
27
|
+
<companyinfo>
|
28
|
+
������������ ����� "������".
|
29
|
+
�������� ������, ������, ���������� ���������� ����� � ��������.
|
30
|
+
���������� �������������� ������ � ���.
|
31
|
+
</companyinfo>
|
32
|
+
<contacts>������ ����������, +7 (960) 782-04-86
|
33
|
+
</contacts>
|
34
|
+
<catalog>
|
35
|
+
<item id="237" thread="515"></item>
|
36
|
+
<item id="943" thread="941"></item>
|
37
|
+
<item id="945" thread="941"></item>
|
38
|
+
<item id="944" thread="941"></item>
|
39
|
+
<item id="946" thread="941"></item>
|
40
|
+
</catalog>
|
41
|
+
<published>10.06.2011</published>
|
42
|
+
<expire>10.07.2011</expire>
|
43
|
+
</job>
|
44
|
+
<job id="11477349">
|
45
|
+
<link>http://msk.superjob.ru/vacancy/?id=11477349</link>
|
46
|
+
<name>������</name>
|
47
|
+
<region>������</region>
|
48
|
+
<salary>�� 18000 ���. � �����</salary>
|
49
|
+
<description>����������� �����������:
|
50
|
+
�������� ���������� �������� ��������.
|
51
|
+
|
52
|
+
���������� � ������������:
|
53
|
+
- ���������������, ������������������, ����������������;
|
54
|
+
- ������ ��.
|
55
|
+
|
56
|
+
������� ������ � �����������:
|
57
|
+
- ������ ������: 5/2 (� 10.00 �� 18.00);
|
58
|
+
- ���������� �����: 15000 ���/��� (�����) + 3000 ���/��� (������ ������� � ���������� ��������)
|
59
|
+
</description>
|
60
|
+
<company id="452220">
|
61
|
+
���
|
62
|
+
</company>
|
63
|
+
<companyinfo>
|
64
|
+
��� - ��� ������ ���������������� ��������, ���������� �� ����� ��������� �����, ���������������� � ����������������, ����������� ���������� � 2002 ����. ��������� ��������� ����� �������� ������ ����� ������������ � ����� ��� 30 ���������� ��������� ���������, �� ������ ������� �������� �� ������� ����� ���������� � ������� �������� ��� ����� 500000 ��������.
|
65
|
+
|
66
|
+
� ������� 9 ��� �������� ������� �����������, ������������ �� ����������� ���� ������ ����������� �������� ���������. � ������ �� � ������������ ����� �������, ��� ��� ����� �������� ��������������� ������ ������������ ������ � ������ ������ �� ����� �������� ����� �� ����� �����������.
|
67
|
+
|
68
|
+
�� ���������� ������ ������ ����� �� ���� ����� ����������� ��� ������� ��� � �����������. �������� ������ � �����������, ������������ �������� ���������� ����������.
|
69
|
+
</companyinfo>
|
70
|
+
<contacts>�������, +7 (495) 775-88-80 ���. 216
|
71
|
+
</contacts>
|
72
|
+
<catalog>
|
73
|
+
<item id="107" thread="5"></item>
|
74
|
+
</catalog>
|
75
|
+
<published>10.06.2011</published>
|
76
|
+
<expire>10.07.2011</expire>
|
77
|
+
</job>
|
78
|
+
<job id="11477307">
|
79
|
+
<link>http://msk.superjob.ru/vacancy/?id=11477307</link>
|
80
|
+
<name>��������� �������</name>
|
81
|
+
<region>������</region>
|
82
|
+
<salary></salary>
|
83
|
+
<description>����������� �����������:
|
84
|
+
��������� ������ � ������ �������.
|
85
|
+
|
86
|
+
���������� � ������������:
|
87
|
+
����������, ������������, ��������������� � ������.
|
88
|
+
|
89
|
+
������� ������ � �����������:
|
90
|
+
���������� �� �� ��.
|
91
|
+
����������� ��.
|
92
|
+
������� �/�, �������� ����������.
|
93
|
+
</description>
|
94
|
+
<company id="234959">
|
95
|
+
��������� "������"
|
96
|
+
</company>
|
97
|
+
<companyinfo>
|
98
|
+
��������� ������� � ��������� ������ � 1957 ����. � ������� ������ � ���� �������� ��������� ����� �������� ��������������� ������� �� ��������� �����.
|
99
|
+
|
100
|
+
�� ������� ��������������� ������� ������� �� ���������� ������ ��������.
|
101
|
+
</companyinfo>
|
102
|
+
<contacts>�������, +7 (499) 187-70-36
|
103
|
+
</contacts>
|
104
|
+
<catalog>
|
105
|
+
<item id="1562" thread="1434"></item>
|
106
|
+
<item id="511" thread="1434"></item>
|
107
|
+
</catalog>
|
108
|
+
<published>10.06.2011</published>
|
109
|
+
<expire>10.07.2011</expire>
|
110
|
+
</job>
|
111
|
+
<job id="11477316">
|
112
|
+
<link>http://krasnodar.superjob.ru/vacancy/?id=11477316</link>
|
113
|
+
<name>��������� �����������</name>
|
114
|
+
<region>���������</region>
|
115
|
+
<salary>�� 20000 ���. � �����</salary>
|
116
|
+
<description>����������� �����������:
|
117
|
+
���������� �� ��� ���������� ������ � ����������-���������� ������� �������������������� ��������
|
118
|
+
|
119
|
+
���������� � ������������:
|
120
|
+
���� ������. �� ������� ������������, ���� ���������������� �++
|
121
|
+
</description>
|
122
|
+
<company id="685434">
|
123
|
+
���-�����������
|
124
|
+
</company>
|
125
|
+
<companyinfo>
|
126
|
+
��� "���-�����������"-��� ��������� ������������� ��������, ���������� 11 ������ 2003�. ���������� �����������, ������������� � ���������: �������������������� ��������, ������������� ������������.
|
127
|
+
</companyinfo>
|
128
|
+
<contacts>���� ��������, +7 (918) 417-03-48
|
129
|
+
</contacts>
|
130
|
+
<catalog>
|
131
|
+
<item id="1248" thread="547"></item>
|
132
|
+
</catalog>
|
133
|
+
<published>10.06.2011</published>
|
134
|
+
<expire>10.07.2011</expire>
|
135
|
+
</job>
|
136
|
+
<job id="11477347">
|
137
|
+
<link>http://slavyansk-na-kubani.superjob.ru/vacancy/?id=11477347</link>
|
138
|
+
<name>����������� (��������������� ������������)</name>
|
139
|
+
<region>��������-��-������</region>
|
140
|
+
<salary>�� 9000 ���. � �����</salary>
|
141
|
+
<description>����������� �����������:
|
142
|
+
���������������� �������� �� �������� ���������������� ������������, ���������� ��������, ������ � ����������� �����������.
|
143
|
+
|
144
|
+
���������� � ������������:
|
145
|
+
|
146
|
+
|
147
|
+
������� ������ � �����������:
|
148
|
+
������ ������: ����������.
|
149
|
+
���������� �����: % �� ������������ ������ �����.
|
150
|
+
���������� ��������.
|
151
|
+
������ � ��������� - ��������� � �. ��������-��-������
|
152
|
+
</description>
|
153
|
+
<company id="16759">
|
154
|
+
��������� ����
|
155
|
+
</company>
|
156
|
+
<companyinfo>
|
157
|
+
��������� � ��������� ���� ������ � ������ ���������������� ������������ ���������, �������� �������� Societe Generale � ������, �������� ���� ������������ � ������� 2004 ����.
|
158
|
+
�� ������ ������� ������� � ������ 2007 ���� ��������� ���� ����� 4 ����� ����� ������ � ������� �� ������������ � ������ ������.
|
159
|
+
������������ ������ ��������� ��������� � ��������� ������������� �����������, ��������� ������������� ���� ������ � ��������, ������� ����������� �� �����, ���������� ��������� ��������� � ������� ��������������. ������������� ������ ������ ��������� ����� ������ ������ Societe Generale ������� �������� ���� � ����� ������ ���������� ���������, ���������� ��������, ��������������, ���������� �������, � ����� ����������� � IT-���������.
|
160
|
+
���� ��� ��������� ������ � ������� � ��������� ������������� �������� � ��� ���������� ����������� ���������� � ����������������� ����� � ����� ���������� ����� - ���� �������� ��� ���.
|
161
|
+
</companyinfo>
|
162
|
+
<contacts>�������� ����, �������� ����, +7 (861) 210-16-01
|
163
|
+
</contacts>
|
164
|
+
<catalog>
|
165
|
+
<item id="56" thread="1"></item>
|
166
|
+
<item id="68" thread="1"></item>
|
167
|
+
</catalog>
|
168
|
+
<published>10.06.2011</published>
|
169
|
+
<expire>10.07.2011</expire>
|
170
|
+
</job>
|
171
|
+
</jobs>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
class MyCrawler < WebCrawler::Base
|
4
|
+
|
5
|
+
target "www.example.com"
|
6
|
+
target "www.example.com/page2"
|
7
|
+
target "www.example.com/page3"
|
8
|
+
target ["www.example.com/contacts", "www.example.com/about"]
|
9
|
+
target "www.example.com/category_:category/page:page/", :category => [1, 2, 3, 4, 5], :page => 1..3 # factory urls
|
10
|
+
target "www.example.com/page2" # would be ignored
|
11
|
+
|
12
|
+
log_to "/tmp/file.log" # or Logger.new(...)
|
13
|
+
|
14
|
+
cache_to '/tmp/wcrawler/cache' # or (CacheClass < CacheAdapter).new *args
|
15
|
+
|
16
|
+
context "job", :jobs do
|
17
|
+
|
18
|
+
map 'link', :to => :source_link, :on => :inner_text # default :on => :inner_text
|
19
|
+
map 'name', :to => :name
|
20
|
+
map 'region', :to => :city_name
|
21
|
+
map 'salary', :to => :profit
|
22
|
+
map 'description', :to => :description, :filter => :format_description
|
23
|
+
map 'contacts', :to => :contact_text
|
24
|
+
map 'company', :to => :company, :on => [:attr, :id]
|
25
|
+
map 'published', :to => :published_at
|
26
|
+
map 'expire', :to => :expire_at
|
27
|
+
map 'catalog item', :to => :specialization_ids, :on => nil, :filter => :convert_specs
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def self.format_description(text)
|
34
|
+
@titles ||= ["Условия работы и компенсации:\n",
|
35
|
+
"Место работы:\n",
|
36
|
+
"Должностные обязанности:\n",
|
37
|
+
"Требования к квалификации:\n"]
|
38
|
+
|
39
|
+
text.each_line.inject("") { |new_text, line| new_text << (@titles.include?(line) ? "<h4>#{line.chomp}</h4>\n" : line) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.convert_specs(specs)
|
43
|
+
@ids_mapping ||= {
|
44
|
+
911 => 4537,
|
45
|
+
1 => 4274,
|
46
|
+
5 => 4335,
|
47
|
+
6 => 4408,
|
48
|
+
16 => [4756, 4545],
|
49
|
+
3 => 4488,
|
50
|
+
9 => 4303,
|
51
|
+
8 => 4649,
|
52
|
+
547 => 4237,
|
53
|
+
579 => 4237,
|
54
|
+
1104 => 4671,
|
55
|
+
10 => 4588,
|
56
|
+
814 => 4568,
|
57
|
+
2 => 4714,
|
58
|
+
11 => 4671,
|
59
|
+
13 => 4691,
|
60
|
+
15 => 4649,
|
61
|
+
17 => 4504,
|
62
|
+
601 => 4428,
|
63
|
+
45 => 4632,
|
64
|
+
22 => 4473,
|
65
|
+
515 => 4524,
|
66
|
+
19 => 4473,
|
67
|
+
20 => 4524,
|
68
|
+
398 => 4749,
|
69
|
+
503 => 4775,
|
70
|
+
941 => 4742,
|
71
|
+
1434 => 4802,
|
72
|
+
2109 => 4537
|
73
|
+
}
|
74
|
+
specs.map { |i| @ids_mapping[i['thread'].to_i] }.to_a.flatten
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
#MyCrawler.run # => return Array
|
81
|
+
#MyCrawler.run(:json) # => return String like a JSON object
|
82
|
+
#MyCrawler.run(:yaml) # => return String of YAML format
|