web_crawler 0.3.1 → 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.
@@ -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 do |a|
15
- normalize(a["href"]).tap do |url|
16
- url = filter.call(url) if url && filter
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)
@@ -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']
@@ -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
- type, encoding = self['Content-Type'].split("=")
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
@@ -1,8 +1,8 @@
1
1
  module WebCrawler
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 3
5
- TINY = 1
4
+ MINOR = 5
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -11,7 +11,7 @@ module WebCrawler::View
11
11
  extend self
12
12
 
13
13
  def factory(type, *args, &block)
14
- const_get(WebCrawler::Utility.camelize(type).to_sym).new(*args, &block)
14
+ (self.name + "::" + type.to_s.classify).constantize.new(*args, &block)
15
15
  end
16
16
 
17
17
  class Base
@@ -14,7 +14,7 @@ module WebCrawler::View
14
14
 
15
15
  def format(item)
16
16
  values = item.respond_to?(:values) ? item.values : item.to_a
17
- values.to_csv(@options)
17
+ values.to_csv(@options[:csv] || {})
18
18
  end
19
19
  end
20
20
  end
@@ -3,7 +3,7 @@ require 'json'
3
3
  module WebCrawler::View
4
4
  class Json < Base
5
5
  def render
6
- {responses: input}.to_json
6
+ input.to_json
7
7
  end
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ require 'yaml'
3
3
  module WebCrawler::View
4
4
  class Yaml < Base
5
5
  def render
6
- YAML.dump(responses: input)
6
+ YAML.dump(input)
7
7
  end
8
8
  end
9
9
  end
@@ -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
+ ������������ ����� &quot;������&quot;.
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
+ ��������� &quot;������&quot;
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
+ ��� &quot;���-�����������&quot;-��� ��������� ������������� ��������, ���������� 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