crawlab_ruby_sdk 0.2.3 → 0.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1208f555fcc371c6da946b5c8bbdc83641a52dbde68533fc00fa41f75569300b
4
- data.tar.gz: 303aaf9afbe4d602c1c30f7cda1b4479f33868cc7caff3032bfad66e0b1800cc
3
+ metadata.gz: bc3fa842f209f649a12b7523fe8ecae913ec3b26f61f4d63c2003327c3a2e7b1
4
+ data.tar.gz: 7e89eb62fb2f2c9fd811ae676ad50d773e9ec7277fc08de022aa51adddd7ba95
5
5
  SHA512:
6
- metadata.gz: be214c103fe025cefb2c95f63a777633f164af57848b64c1ffbcd1648425023d1486906bd1637df7652d1edc91ef703705aa31a3e02dac0ac92e92122f0e8a40
7
- data.tar.gz: ab4616543c95a265bcba4cc2671e54d456ae75dcb90e693d03fd5fca695ce1f7bbfe2076b2b1a5e95cf27ecd6f4fa51f58277650a8f8ba4f9a2179f6bed3241c
6
+ metadata.gz: 11b6e372e7f90a06b84d9e82033b1cbfb05ee105bb2182544bb2da3f930e4263908385262cc718ed28fc7e4d905259b586855520fd6cca9122027fbb4c3bad91
7
+ data.tar.gz: d5d6ebeb5de3dcc5172c57d9173da98f602350eddeddf4707b5ea4981f1898a471cc8378583409286db5408a0847e9c753d0caaa9aa2cbcf96482a52da454279
@@ -15,6 +15,13 @@ Gem::Specification.new do |spec|
15
15
  spec.add_dependency 'google-protobuf','~> 3.23.2'
16
16
  spec.add_dependency 'json','~> 2.6.3'
17
17
  spec.add_dependency 'aliyun-sdk','~> 0.8.0'
18
+ spec.add_dependency 'rest-client', '~> 2.1.0'
19
+ spec.add_dependency 'nokogiri','~> 1.15.3'
20
+ spec.add_dependency 'therubyracer','~> 0.12.3'
21
+ spec.add_dependency 'faraday','~> 2.7.10'
22
+ spec.add_dependency 'activesupport','~> 7.0.6'
23
+ spec.add_dependency 'hpricot','~> 0.8.6'
24
+ spec.add_dependency 'htmlentities','~> 4.3.4'
18
25
 
19
26
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
27
 
@@ -1,3 +1,3 @@
1
1
  module CrawlabRubySdk
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
@@ -1,6 +1,14 @@
1
1
  # require "crawlab_ruby_sdk/version"
2
2
  require "grpc"
3
3
  require "json"
4
+ require "faraday"
5
+ require 'therubyracer'
6
+ require "nokogiri"
7
+ require "rest-client"
8
+ require "active_support"
9
+ require "htmlentities"
10
+ require "hpricot"
11
+
4
12
  def traverse_dir(file_path)
5
13
  if File.directory? file_path
6
14
  Dir.foreach(file_path){|file| traverse_dir(file_path+"/"+file) if file!="." and file!=".." }
@@ -0,0 +1,219 @@
1
+ class CommentMethod
2
+
3
+ # 页面503 需要验证时 获取cookie
4
+ def self.chk_jschl(params)
5
+ cxt = V8::Context.new
6
+ url = params[:url]
7
+ sub_site_url = params[:sub_site_url]
8
+ header = params[:header]
9
+
10
+ conn = Faraday.new(:url => url)
11
+ res = conn.get do |req|
12
+ req.url url
13
+ req.headers = header
14
+ end
15
+ cookie = res.headers["set-cookie"].split(";")[0]
16
+ doc = Hpricot(res.body)
17
+ k_pre = doc.to_s.match(/k = \'(.*?)\';/)[1] rescue nil
18
+ k_int= doc.to_s.match(/;k\+=(.*?);/)[1] rescue nil
19
+ k_int = cxt.eval(k_int).to_s
20
+ p = doc.search("##{k_pre+k_int}").inner_html rescue nil
21
+ line1 = doc.to_s.match(/var s,t,o,p.*?,f(.+?);/).to_s
22
+ line2 = doc.to_s.match(/;(.+?); '; 121'/)[1].to_s.gsub("t.length", sub_site_url.size.to_s).gsub("a.value = ", "") rescue ""
23
+ # line2 = doc.to_s.match(/;(.+?); '; 121'/)[1].to_s
24
+ line3 = line2.gsub(/function\(p\)\{var .*?return \+\(p\)}\(\)/,"+(#{p})")
25
+ line4 = line3.gsub(/function\(p\).*?\)\);/,"'#{sub_site_url}'.charCodeAt(1)));")
26
+ jschl_answer = cxt.eval(line1+line4).to_s
27
+
28
+ r = doc.search("input[@name=r]")[0][:value]
29
+ jschl_vc = doc.search("input[@name=jschl_vc]")[0][:value]
30
+ pass = doc.search("input[@name=pass]")[0][:value]
31
+
32
+ # form_params = doc.search("form#challenge-form input").map{|x| [x["name"],x["value"]]}.to_h
33
+ # form_params["jschl_answer"] = jschl_answer if jschl_answer.present?
34
+ # query = URI.encode_www_form(form_params)
35
+
36
+ query = URI.encode_www_form([['r', r], ['jschl_vc', jschl_vc], ['pass', pass], ['jschl_answer', jschl_answer],['cf_ch_verify','plat']])
37
+
38
+
39
+ link = doc.search("form#challenge-form")[0]["action"]
40
+ sleep 4
41
+ res1 = conn.post do |req|
42
+ req.url link
43
+ req.headers = header
44
+ req.headers["cookie"]= cookie
45
+ req.body = query
46
+ end
47
+ res1.headers["set-cookie"]
48
+
49
+ if res1.headers["set-cookie"].nil?
50
+ return nil
51
+ else
52
+ cf_clearance = res1.headers["set-cookie"].split(";")[0].split("=")[1]
53
+ cookie += "; cf_clearance=#{cf_clearance}"
54
+ return cookie
55
+ end
56
+ end
57
+
58
+
59
+ def self.get_email(encode_email)
60
+ cxt = V8::Context.new
61
+ js_str = "function r(e, t) {\n\tvar r = e.substr(t, 2);\n\treturn parseInt(r, 16)\n}\n\nfunction n(n, c) {\n\t\tfor (var o = \"\", a = r(n, c), i = c + 2; i < n.length; i += 2) {\n\t\t\tvar l = r(n, i) ^ a;\n\t\t\to += String.fromCharCode(l)\n\t\t}\n\t\ttry {\n\t\t\to = decodeURIComponent(escape(o))\n\t\t} catch (u) {\n\t\t\te(u)\n\t\t}\n\t\treturn o\n}\n\n\nn(\"#{encode_email}\",28)"
62
+ email = cxt.eval(js_str) rescue nil
63
+ return email
64
+ end
65
+
66
+ def self.get_token(ts)
67
+ secret = "henttlxnn"
68
+ key = "getsetredis"
69
+ Digest::MD5.hexdigest("#{key}#{secret}#{ts}")
70
+ end
71
+
72
+ def self.french_month(month)
73
+ {
74
+ "janvier" => "January",
75
+ "février" => "February",
76
+ "mars" => "March",
77
+ "avril" => "April",
78
+ "mai" => "May",
79
+ "juin" => "June",
80
+ "juillet" => "July",
81
+ "août" => "August",
82
+ "aout" => "August",
83
+ "septembre" => "September",
84
+ "octobre" => "October",
85
+ "novembre" => "November",
86
+ "décembre" => "December",
87
+ }[month]
88
+ end
89
+
90
+ def self.es_month(month)
91
+ {
92
+ "enero"=> "January",
93
+ "febrero"=> "February",
94
+ "marzo"=> "March",
95
+ "abril"=> "April",
96
+ "mayo"=> "May",
97
+ "junio"=> "June",
98
+ "julio"=> "July",
99
+ "agosto"=> "August",
100
+ "septiembre"=> "September",
101
+ "octubre"=> "October",
102
+ "noviembre"=> "November",
103
+ "diciembre"=> "December"
104
+ }[month]
105
+ end
106
+
107
+ def self.it_month(month)
108
+ {
109
+ "gennaio"=> "January",
110
+ "febbraio"=> "February",
111
+ "marzo"=> "March",
112
+ "aprile"=> "April",
113
+ "maggio"=> "May",
114
+ "giugno"=> "June",
115
+ "luglio"=> "July",
116
+ "agosto"=> "August",
117
+ "settembre"=> "September",
118
+ "ottobre"=> "October",
119
+ "novembre"=> "November",
120
+ "dicembre"=> "December"
121
+ }[month]
122
+ end
123
+
124
+ def self.es_simple_month(month)
125
+ {
126
+ "ene"=> "January",
127
+ "feb"=> "February",
128
+ "mar"=> "March",
129
+ "abr"=> "April",
130
+ "may"=> "May",
131
+ "jun"=> "June",
132
+ "jul"=> "July",
133
+ "ago"=> "August",
134
+ "sep"=> "September",
135
+ "oct"=> "October",
136
+ "nov"=> "November",
137
+ "dic"=> "December"
138
+ }[month]
139
+ end
140
+
141
+ def self.ar_month(month)
142
+ {
143
+ "يناير"=> "January",
144
+ "فبراير"=> "February",
145
+ "مارس"=> "March",
146
+ "أبريل"=> "April",
147
+ "مايو"=> "May",
148
+ "يونيو"=> "June",
149
+ "يوليو"=> "July",
150
+ "أغسطس"=> "August",
151
+ "سبتمبر"=> "September",
152
+ "أكتوبر"=> "October",
153
+ "نوفمبر"=> "November",
154
+ "ديسمبر"=> "December"
155
+ }[month]
156
+ end
157
+
158
+ def self.ar_month2(month)
159
+ {
160
+ "جانفي"=> "January",
161
+ "فيفري"=> "February",
162
+ "مارس"=> "March",
163
+ "أفريل"=> "April",
164
+ "ماي"=> "May",
165
+ "جوان"=> "June",
166
+ "جويلية"=> "July",
167
+ "أوت"=> "August",
168
+ "سبتمبر"=> "September",
169
+ "أكتوبر"=> "October",
170
+ "نوفمبر"=> "November",
171
+ "ديسمبر"=> "December"
172
+ }[month]
173
+ end
174
+
175
+
176
+ def self.th_month(month)
177
+ {"มกราคม"=>"01", "กุมภาพันธ์"=>"02", "มีนาคม"=>"03", "เมษายน"=>"04", "พฤษภาคม"=>"05", "มิถุนายน"=>"06", "กรกฎาคม"=>"7", "สิงหาคม"=>"08", "กันยายน"=>"09", "ตุลาคม"=>"10", "พฤศจิกายน"=>"11", "ธันวาคม"=>"12"}[month]
178
+ end
179
+
180
+ def self.id_month(month)
181
+ {
182
+ "Januari" => "January",
183
+ "Februari" => "February",
184
+ "Maret" => "March",
185
+ "April" => "April",
186
+ "Mei" => "May",
187
+ "Juni" => "June",
188
+ "Juli" => "July",
189
+ "Agustus" => "August",
190
+ "September" => "September",
191
+ "Oktober" => "October",
192
+ "November" => "November",
193
+ "Desember" => "December",
194
+ }[month]
195
+ end
196
+
197
+ def self.th_year(year)
198
+ year = year.to_i - 543
199
+ return year
200
+ end
201
+
202
+ def self.ru_month(month)
203
+ months = {
204
+ 'января'=> 'January',
205
+ 'февраля'=> 'February',
206
+ 'марта'=> 'March',
207
+ 'апреля'=> 'April',
208
+ 'мая'=> 'May',
209
+ 'июня'=> 'June',
210
+ 'июля'=> 'July',
211
+ 'августа'=> 'August',
212
+ 'сентября'=> 'September',
213
+ 'октября'=> 'October',
214
+ 'ноября'=> 'November',
215
+ 'декабря'=> 'December'
216
+ }[month]
217
+ end
218
+
219
+ end
@@ -0,0 +1,207 @@
1
+ class Htmlarticle
2
+
3
+ def initialize(text,options = {})
4
+ @text = text
5
+ @options = options
6
+ @content = ""
7
+ end
8
+
9
+ def content
10
+ @content
11
+ end
12
+
13
+ # 参数说明
14
+ # doc 源代码 必填参数
15
+ # content_selector 正文规则 必填参数
16
+ # content_replacer 正文替换正则
17
+ # content_filter 正文过滤
18
+ # content_rid_html_selector 正文剔除html标签
19
+ # html_replacer html换行标签
20
+ # html_replacer_for_no_tag_line 无标签文字是否按照同级换行标签换行 0 不处理 1 换行处理
21
+ # params = {doc:doc,content_selector:content_selector,content_rid_html_selector:content_rid_html_selector,html_replacer:html_replacer,html_replacer_for_no_tag_line:html_replacer_for_no_tag_line,content_replacer:content_replacer,content_filter:content_filter}
22
+ # 示例用法
23
+ # doc = Nokogiri::HTML(res.body)
24
+ # content_selector = "div.content"
25
+ # html_replacer = "p"
26
+ # params = {doc:doc,content_selector:content_selector,html_replacer:html_replacer}
27
+ # desp,html_content = Htmlarticle.get_html_content(params)
28
+ def self.get_html_content(params)
29
+ desp_buff,html_content,desp = "","",""
30
+ doc = params[:doc]
31
+ content_selector = params[:content_selector].to_s.split("||||")
32
+ html_replacer = params[:html_replacer].to_s.split("||||")
33
+ html_replacer_for_no_tag_line = params[:html_replacer_for_no_tag_line]
34
+ content_rid_html_selector = params[:content_rid_html_selector].to_s.split("||||")
35
+ content_selector.each do |v|
36
+ doc_content = doc.clone
37
+ html_content = ""
38
+ doc_content.search(v).each do |s|
39
+ # 剔除不需要的节点
40
+ content_rid_html_selector.each do |rid|
41
+ s.search(rid).remove
42
+ end
43
+ # 处理html_content
44
+ html_content += s.to_s if s.present?
45
+ end
46
+ # 处理 desp
47
+ doc_content.search(v).each do |s|
48
+ if html_replacer.count > 0 && html_replacer[0].present?
49
+ if html_replacer.include? s.name
50
+ desp_buff += "\n"
51
+ end
52
+ end
53
+ s.children.each do |n|
54
+ desp_buff = get_desp(n,desp_buff,html_replacer,html_replacer_for_no_tag_line)
55
+ end
56
+ end
57
+ # 处理空格和换行
58
+ # desp_buff = desp_buff.gsub("\n","").strip
59
+ break if html_content.present? && desp_buff.present?
60
+ end
61
+
62
+ filters = params[:content_filter].to_s.split("||||")
63
+ filters.each do |filter|
64
+ if desp_buff.include? filter
65
+ desp_buff = ""
66
+ html_content = ""
67
+ break
68
+ end
69
+ end
70
+
71
+ content_replacer = params[:content_replacer].to_s
72
+ if content_replacer.present?
73
+ content_replacer.split("||||").each do |replacer|
74
+ desp_buff = desp_buff.gsub(replacer,"") if replacer.present?
75
+ if replacer.present?
76
+ replacer_arr = replacer.split("&&&&")
77
+ desp_buff = desp_buff.gsub(replacer_arr[0],"")
78
+ html_content = html_content.gsub(replacer_arr[1],"") if replacer_arr[1].present?
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+
85
+ desp = ""
86
+ desp_buff.split("\n").each do |v|
87
+ desp += v.strip + "\n" if v.strip.present?
88
+ end
89
+
90
+ return desp,html_content
91
+ end
92
+
93
+ def self.get_desp(n,desp_buff,html_replacer,html_replacer_for_no_tag_line)
94
+ html_replacer = html_replacer
95
+ if html_replacer.count > 0 && html_replacer[0].present?
96
+ if html_replacer.include? n.name
97
+ desp_buff += "\n"
98
+ end
99
+ end
100
+ if n.name == "text"
101
+ if html_replacer_for_no_tag_line == 1 && (html_replacer.count > 0 && html_replacer[0].present?)
102
+ if n.parent.first_element_child != n
103
+ if html_replacer.include? n.previous_sibling.try(:name)
104
+ desp_buff += "\n"
105
+ end
106
+ end
107
+ end
108
+ desp_buff += n.inner_text.gsub("\n"," ") if n.inner_text.present?
109
+ if !(html_replacer.count > 0 && html_replacer[0].present?)
110
+ desp_buff += "\n"
111
+ end
112
+ end
113
+ if n.children.present?
114
+ n.children.each do |c|
115
+ desp_buff = get_desp(c,desp_buff,html_replacer,html_replacer_for_no_tag_line)
116
+ end
117
+ end
118
+ return desp_buff
119
+ end
120
+
121
+
122
+
123
+ def title
124
+
125
+ end
126
+
127
+ def parse
128
+ text = @text.to_s.gsub(/(?imx)<!--.*?-->/,"").gsub(/(?imx)<script.+?script>/,"").gsub(/(?imx)<style.+?style>/,"").gsub(/<\/a>/,"</a>\n")
129
+ preTextLen = 0
130
+ startPos = -1
131
+ _depth = 6
132
+ _limitCount = 180
133
+ _headEmptyLines = 2
134
+ _endLimitCharCount = 20
135
+
136
+ if text.split("\n").count < 10
137
+ text = text.gsub(">",">\n")
138
+ end
139
+ #puts text
140
+ body = text.match(/(?imx)<body.+?<\/body>/).to_s
141
+
142
+ #body = body.gsub(/(?imx)(<[^<>]+\n.+?>)/,"\1")
143
+
144
+ # body.scan(/(?imx)(<[^<>]+\n.+?>)/).each_with_index do |n,i|
145
+ # puts "-----#{i}"
146
+ # puts n
147
+ # x = n.to_s.gsub("\n","")
148
+ # body = body.gsub(/(?imx)#{n}/,x)
149
+ # end
150
+
151
+ orgLines = body.gsub(/(?imx)<\/p>|<br.+?\/>/,"[crlf]").gsub(/(?imx)<(\S*?)[^>]*>.*?|<.*? \/>/,"").split("\n")
152
+ lines = []
153
+
154
+ @content = ""
155
+
156
+ orgLines.each do |line|
157
+ lines << line.strip
158
+ end
159
+
160
+ #puts lines.join("\n")
161
+
162
+ for i in 0..(lines.count-_depth-1)
163
+ len = 0
164
+ for j in 0..(_depth-1)
165
+ len += lines[i+j].size
166
+ end
167
+
168
+ if startPos == -1
169
+ if preTextLen > _limitCount && len > 0
170
+ emptyCount = 0
171
+ k = i - 1
172
+ k.downto 1 do |z|
173
+ if lines[z].to_s == ""
174
+ emptyCount+=1
175
+ else
176
+ emptyCount = 0
177
+ if emptyCount == _headEmptyLines
178
+ startPos = z + _headEmptyLines
179
+ break
180
+ end
181
+ end
182
+ end
183
+ if startPos == -1
184
+ startPos = i
185
+ end
186
+ for j in startPos..i
187
+ @content+= lines[j]
188
+ end
189
+ end
190
+ else
191
+ if len <= _endLimitCharCount && preTextLen < _endLimitCharCount
192
+ #break
193
+ startPos = -1
194
+ end
195
+ @content+= lines[i]
196
+ end
197
+ preTextLen = len
198
+ end
199
+
200
+ @content = @content.gsub("[crlf]","\n")
201
+
202
+ @content
203
+ end
204
+
205
+
206
+
207
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crawlab_ruby_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - min
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-07-16 00:00:00.000000000 Z
11
+ date: 2023-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grpc
@@ -66,6 +66,104 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.8.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rest-client
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.1.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.1.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: nokogiri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.15.3
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.15.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: therubyracer
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.12.3
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.12.3
111
+ - !ruby/object:Gem::Dependency
112
+ name: faraday
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 2.7.10
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 2.7.10
125
+ - !ruby/object:Gem::Dependency
126
+ name: activesupport
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 7.0.6
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 7.0.6
139
+ - !ruby/object:Gem::Dependency
140
+ name: hpricot
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.8.6
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.8.6
153
+ - !ruby/object:Gem::Dependency
154
+ name: htmlentities
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 4.3.4
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 4.3.4
69
167
  description: Write a longer description or delete this line.
70
168
  email:
71
169
  - lijinmin3903@126.com
@@ -93,6 +191,8 @@ files:
93
191
  - lib/entity/stream_message_code_pb.rb
94
192
  - lib/entity/stream_message_pb.rb
95
193
  - lib/models/base.rb
194
+ - lib/models/common_method.rb
195
+ - lib/models/htmlarticle.rb
96
196
  - lib/models/thinktank_expert.rb
97
197
  - lib/models/thinktank_expert_report.rb
98
198
  - lib/models/thinktank_information.rb