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.
@@ -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