構建自己的DSL之二 抓取文本處理

轉載請標明出處:http://fuliang.iteye.com/blog/1122051

公司的蜘蛛抓取的內容一個記錄是以TAB分割的各個字段的值,並且隨着各種分類得分、正文靜態得分策略的添加,版本不斷的演變。每次做抽樣、分析、分類語料等文本處理都需要寫一些樣板式的代碼,並且得到wiki查找指定版本每個字段的位置。構建一個好的DSL來自動處理這件事情能夠省去很多重複的操作,只需要關注要處理的事情即可。
我們想提供簡單自然的API來做事情,我們常用的需求有:
1、每次版本變更幾乎不需要修改代碼、只需要添加配置文件,比如新版本增加一個
travel_confidence,我們不需要修改代碼就可以使用:

crawler_file.find_by_travel_confidence(90)
crawler_file.find_by_travel_confidence_gt(50)
...

2、可以自動的識別版本、並得到版本號:

crawler_file.version

3、按照次序給出各個字段的名字:

crawler_file.field_names

4、支持模糊查詢字段的名字:

crawler_file.grep_fields(/url/)

5、根據某個字段的模糊或者精確的值來在一個文件中查找記錄

#根據host來查找記錄
crawler_file.find_by_host("www.9tour.cn") do |record|
printf("%s\t%s\n", record.title, record.host)
end
#根據標題的字段來模糊查找
crawler_file.find_by_title_like(/線路/) do |record|
puts record.title
end

6、數字的字段我們需要支持根據大小關係來查找記錄:比如gt(>)、ge(>=)
eq(=)、le(<=)、lt(<)

#content_confidence大於50的記錄
crawler_file.find_by_content_confidence_gt(50) do |record|
printf("%s\t%s\n", record.title, record.content_confidence)
end

7、比較複雜的需求,我們可以寫一些字段小過濾器,來找到需要的記錄:

filter = lambda{|host,title| host == "www.9tour.cn" && title =~ /線路/}

crawler_file.find_by_fields([:host,:title],filter) do |record|
printf("%s\t%s\n", record.host,record.title)
end

8.我們需要代碼DRY。

我們下面看看如何完成這個功能,首先我們可以使用yaml來配置版本以及記錄對應的字段:

v1:
download_time: 0
host: 1
url: 2
url_md5: 3
parent_url_md5: 4
crawl_level: 5
loading_time: 6
anchor_text: 7
title: -4
keywords: -3
description: -2
content: -1
v2:
download_time: 0
host: 1
url: 2
url_md5: 3
parent_url_md5: 4
crawl_level: 5
loading_time: 6
http_code: 7
content_confidence: 8
anchor_text: 9
title: -4
keywords: -3
description: -2
content: -1
...#中間省略

v9:
download_time: 0
host: 1
url: 2
url_md5: 3
parent_url_md5: 4
crawl_level: 5
publish_time: 6
http_code: 7
content_confidence: 8
list_confidence: 9
feeling_confidence: 10
travel_confidence: 11
qnc_cat: 12
qnc_chi: 13
qnc_zhu: 14
qnc_xing: 15
qnc_you: 16
qnc_gou: 17
qnc_le: 18
anchor_text: 19
raw_title: -10
title: -9
keywords: -8
description: -7
content: -6
lda_tag: -5
location_text: -4
location_confidence: -3
hotel_confidence: -2
gonglue_confidence: -1

以及是各個版本是數字字段的版本集合:

num_fields:
- download_time
- crawl_level
- publish_time
- content_confidence
- list_confidence
- feeling_confidence
- travel_confidence
- hotel_confidence
- gonglue_confidence

功能一:根據字段數來簡單識別版本:

class VersionDetector
@@field_num_version_map = {
12 => 1,
14 => 2,
15 => 3,
24 => 4,
25 => 5,
16 => 6,
26 => 7,
27 => 8,
30 => 9
};

class << self
def detect(file)
version = -1
if file.is_a?(String) then
line = File.open(file) do |file| file.gets end
version = @@field_num_version_map[line.split(/\t/).size]
elsif file.is_a?(File) then
before_pos = file.pos
file.seek(0)
line = file.gets
version = @@field_num_version_map[line.split(/\t/).size]
file.seek(before_pos)
else
raise ArgumentError.new 'Argument type: #{file.class} is error, must be a String or File type'
end

raise Exception.new 'Unkown version file format' if version.nil?

return version
end
end
end

我們通過yaml來load版本配置:

require 'yaml'

class FieldConfig
attr_reader :fields_map, :num_fields

def initialize(version)
config = YAML.load_file 'conf.yml'
@fields_map = config["v#{version}"]
@num_fields = config["num_fields"]
end
end

我們根據配置文件動態的定義記錄的字段,這樣我們修改字段,不需要修改代碼:

class CrawlerRecord
def self.config(field_config)
@@field_config = field_config
attr_reader *(field_config.fields_map.keys) #動態定義字段的讀方法
end

def initialize(raw_line)
@raw_line = raw_line
fields = raw_line.split(/\t/)
@@field_config.fields_map.each do |key,value|#動態設置各個字段的值
instance_variable_set("@" + key.to_s,fields[value])
end
end

def raw
@raw_line
end
end

我們寫一個CrawlerFile類來支持上面描述的一些功能:

class CrawlerFile

end

在這個類中定義數字字段支持的關係操作符:

@@num_fields_op = {
:gt => ">",
:lt => "<",
:eq => "=",
:ge => ">=",
:le => "<="
};

字段和版本的讀取方法:

attr_reader :field_names, :version

定義初始化方法:

def initialize(path)
@file = File.new(path) #對應的文件
@version = VersionDetector.detect(@file) #得到版本信息
@@field_config = FieldConfig.new(@version) #得到該版本的配置
@field_names = @@field_config.fields_map.keys #根據配置文件得到字段名字
CrawlerRecord.config(@@field_config) #配置CrawlerRecord動態生成字段讀方法
define_help_method #定義幫助方法,來完成上面列舉的其他功能
end

實現define_help_method

def define_help_method
CrawlerFile.class_eval do
#根據配置文件動態定義按照一個字段模糊查找方法find_by_xxx_like
@@field_config.fields_map.keys.each do |field|
define_method :"find_by_#{field}_like" do |regex,&block|
if block.nil? then
lines = []
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
lines << line if line.send(field) =~ regex
end
lines
else
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
block.call(line) if line.send(field) =~ regex
end
end
@file.seek(0)
end
#根據配置文件動態定義按照一個字段模糊查找方法find_by_xxx
define_method :"find_by_#{field}" do |value,&block|
if block.nil? then
lines = []
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
lines << line if line.send(field) == value
end
lines
else
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
block.call(line) if line.send(field) == value
end
end
@file.seek(0)
end
end
#爲所有的數字字段動態定義按照大小關係查找的方法:
@@field_config.num_fields.each do |field|
next if not @@field_config.fields_map[field]

@@num_fields_op.keys.each do |op|
define_method :"find_by_#{field}_#{op.to_s}" do |value,&block|
op_val = @@num_fields_op[op]
if block.nil? then
lines = []
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
field_val = line.send(field)
lines << line if eval("#{field_val} #{op_val} #{value}")
end
lines
else
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
field_val = line.send(field)
block.call(line) if eval("#{field_val.to_i} #{op_val} #{value}")
end
end
@file.seek(0)
end
end
end
end
end


支持字段的組合的查詢:

def find_by_fields(fields,cond_checker)
if block_given? then
@file.each_line do |raw_line|
line = CrawlerRecord.new(raw_line)
yield line if cond_checker.call(*fields.collect{|field| line.send(field) })
end
else
lines = []
@file.each_line do |line|
line = CrawlerRecord.new(raw_line)
lines << line if cond_checker.call(*fields.collect{|field| line.send(field)})
end
lines
end
@file.seek(0)
end

關閉文件:

def close
@file.close
end
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章