在Ruby中解析街道地址

6
我正在为数据库处理地址并将它们转换成相应的字段格式。我可以得到房屋号码和街道类型,但是还在尝试确定最佳方法来获取没有数字和最后一个单词的街道。标准的街道地址格式如下:
    res[:address] = '7707 Foo Bar Blvd'

目前我能解析以下内容:

    house = res[:address].gsub(/\D/, '')
    street_type = res[:address].split(/\s+/).last

我的第一个挑战是如何获取“Foo Bar”。请注意,街道名称可能是一、两个或三个单词。我正在努力在Ruby中找到一行表达式的解决方案。

我的第二个问题是如何改进“house”代码以处理末尾带有字母的房屋编号。例如,“7707B”。

最后,如果您能引用一个具有这些表达式示例的好的速查表,那将非常有帮助。


4
简单来说,不要这样做!要么逐个请求字段,要么将它们作为整体存储。对此进行解析永远不可能达到100%的准确性,因为变化的程度超出了你所能想象的范围。如果你想要一个房号字段(其实并不应该),就在表单中加入一个house_number字段。 - drhenner
2
顺便提一下:USPS有一个API。https://www.usps.com/business/web-tools-apis/welcome.htm他们可以验证您的地址,并可能向您提供更详细的信息。 - drhenner
我要修改的基础属性数据库以这种方式分隔字段,这就是为什么有房屋和街道类型字段。我也有完整的街道地址。我正在尽可能保持数据一致性。 - Stuart C
5个回答

11

如果可能的话,我建议使用一个库来完成这个任务,因为地址解析很困难。看一下Indirizzo Ruby gem,它可以让这个过程变得简单:

require 'Indirizzo'
address = Indirizzo::Address.new("7707 Foo Bar Blvd")
address.number
 => "7707"
address.street
 => ["foo bar blvd", "foo bar boulevard"] 

即使您不使用Indirizzo库本身,阅读其源代码可能非常有用,以了解他们如何解决问题。例如,它具有经过精细调整的正则表达式,以匹配地址的不同部分:

Match = {
  # FIXME: shouldn't have to anchor :number and :zip at start/end
  :number   => /^(\d+\W|[a-z]+)?(\d+)([a-z]?)\b/io,
  :street   => /(?:\b(?:\d+\w*|[a-z'-]+)\s*)+/io,
  :city     => /(?:\b[a-z][a-z'-]+\s*)+/io,
  :state    => State.regexp,
  :zip      => /\b(\d{5})(?:-(\d{4}))?\b/o,
  :at       => /\s(at|@|and|&)\s/io,
  :po_box => /\b[P|p]*(OST|ost)*\.*\s*[O|o|0]*(ffice|FFICE)*\.*\s*[B|b][O|o|0][X|x]\b/
}

这些文件可以从源代码中提供更详细的信息:

(但我也基本同意@drhenner的评论,为了让自己更容易,您可能只需在单独的字段中接受这些数据输入。)

编辑:为了更具体地回答如何删除街道后缀(例如“Blvd”),您可以使用Indirizzo的正则表达式常量(例如constants.rb中的Suffix_Type)进行操作:

address = Indirizzo::Address.new("7707 Foo Bar Blvd", :expand_streets => false)
address.street.map {|street| street.gsub(Indirizzo::Suffix_Type.regexp, '').strip }
 => ["foo bar"]

(请注意,我还向初始化程序传递了 :expand_streets => false ,以避免在丢弃后缀的情况下扩展“Blvd”和“Boulevard”这两个替代项。)

1
Stuart,我查看了这个 gem,但是我没有看到它处理如何获取没有街道类型的街道地址。再一次地,只要处理尝试匹配数据字段。我也有完整的街道地址,以便更容易地提取内容。 - Stuart C
Indirizzo似乎没有一个内置的方法来分离街道名称和后缀(例如“Blvd”)。但它有这些后缀作为常量(例如constants.rb中的Indirizzo :: Suffix_Type)。你可以使用这些来解析出后缀。我已经更新了我的回答,并提出了如何做到这一点的建议。 - Stuart M
Stuart 我会尝试一下,但是检查 Gem 是否符合要求需要一些时间,比之前做的要多。看起来这可能有效,也许我只需切换我的以前的代码。测试后再进行纠正。暂时就这样。谢谢。 - Stuart C
好的。如果您有任何其他问题/疑问,请告诉我,如果最终解决了您的问题,请考虑将问题标记为已接受,谢谢。 - Stuart M

2
你可以在正则表达式中随意使用命名捕获组。
matches = res[:address].match(/^(?<number>\S*)\s+(?<name>.*)\s+(?<type>.*)$/)
number = matches[:number]
house = matches[:name]
street_type = matches[:type]

如果您希望正则表达式更加准确地匹配类型,您可以将 (?<type>.*) 替换为 (?<type>(Blvd|Ave|Rd|St)) 并添加所需的所有不同选项。


1
你可以尝试使用以下正则表达式: ^\S+ (.+?) \S+$ 其中,\S 匹配任何非空格字符,^ 匹配字符串开头,$ 匹配字符串结尾。而 (.+?) 则捕获两者之间的任何内容。

0

目前我只是将我所得到的信息传递给Google Maps,然后让它们返回一个易于解析的格式化街道地址。

function addressReview(addressInput) {
geocoder = new google.maps.Geocoder();
var latlng = new google.maps.LatLng(-34.397, 150.644);
geocoder.geocode( { 'address': addressInput}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
  if (results[0]) {
    var addr = results[0].formatted_address;
    var latTi = results[0].geometry.location.lat();
    var lonGi = results[0].geometry.location.lng();
    $.post('/welcome/gcode',{ add: addr , la: latTi , lo: lonGi });
    $('#cust_addy').val(addr);
  } else {
    $('#cust_addy').attr("placeholder",'Cannnot determine location');
  }
} else {
  $('#cust_addy').attr("placeholder",'Cannnot determine location');
}
});
}

之后,我只是在Ruby中使用.split(',')和.split(' ')将其拆分。

0

仔细检查您的数据集,确保这个问题是否已经为您处理过。

我花了相当多的时间首先创建一个可能街道名称结尾的分类法,使用正则表达式条件来尝试从完整地址字符串中拔出街道号码和其他所有内容,结果发现我的形状文件的属性表已经将这些组件分段了。

在您继续解析地址字符串的过程之前,一定要确保您的数据集没有已经为您完成此操作!这通常是一项繁琐的任务,因为地址字符串不可避免地会有奇怪的变化(例如某些包裹地址是针对内陆包裹的,具有奇怪的地址等),所以请务必检查!


但如果你没有,可以通过地址字符串运行address.split(" ")创建一个“单词”的数组。在大多数情况下,第一个“单词”是街道号码。这对我约95%的地址有效。(注意:我的:地址字符串不包含城市、县、州、邮政编码,它们只是本地地址)

我遍历了整个地址库,并从每个地址中取出了最后一个“单词”,并检查了这个数组,并取出了任何不是“Lane”、“Road”、“Rd”或其他的“单词”。从这个地址结尾列表中,我创建了这个巨大的匹配正则表达式对象。

streetnm_endings = street_endings.map {|s| /#{s}/ }
endings_matches = Regexp.union(street_endings)

我遍历了每个地址字符串,通过 shift 来去掉第一个数组成员,因为通常这是街道号码。然后 gsub 掉街道结尾部分,得到不包含街道号码或街道结尾的街道名称,这通常是数据库不喜欢的内容:
parcels.each do |p|
  remainder = p.address.split(" ")
  p.streetnum = remainder.shift
  p.streetname = remainder.join(" ").gsub(endings_matches, "")
  p.save
end

它并不总是有效的,但大部分时间都有效。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接