#191 Mechanize
- Download:
- source codeProject Files in Zip (93.6 KB)
- mp4Full Size H.264 Video (19.3 MB)
- m4vSmaller H.264 Video (12 MB)
- webmFull Size VP8 Video (29.8 MB)
- ogvFull Size Theora Video (29.2 MB)
上一集我们使用Nokogiri抓取单个HTML页面的内容. 如果有更复杂的抓取需求,像需要先登陆才能抓取数据的,这种简单的方法就行不通了,所以这次我们使用Mechanize来交互网站,抓取数据.
我们将要使用的网站是Ta-da list. 它是37 Signals的一个to_do list应用. 我们已经注册了一个帐号,并创建了一个清单列表. 如果想再次查看这个列表,就必须先登陆这个站点,然后点击页面上的清单名称.
现在需要将清单内容自动导入到rails应用的商品列表. 因此我们需要交互这个Ta-da List,得到这些商品,然后就可以用上一集写的脚本来来获取每个商品价格.
由于清单页面是私人的,我们不能访问列它的URL. 使用curl
请求页面,会看到下面的内容.
<p>所以我们不能直接访问清单页面. 访问前必须要先登陆应用. 这时候就需要用到Mechanize了. Mechanize使用Nokogiri,并扩展了一些其他的功能来交互网站,可以像点击链接,提交表单一样用来处理一些任务.</p> <p>Mechanize跟一般的gem一样安装:</p> ``` terminal sudo gem install mechanize
安装完成后,可以打开一个Rails console看看它是怎么工作的. 首先,需要引用Mechanize.
``` terminal >> require 'mechanize' => []<p>接下来,需要实例化一个Mechanize agent:</p> ``` terminal > agent = WWW::Mechanize.new => #<WWW::Mechanize:0x101c74780 @follow_meta_refresh=false, @proxy_addr=nil, @digest=nil, @watch_for_set=nil, @html_parser=Nokogiri::HTML, @pre_connect_hook=#<WWW::Mechanize::Chain::PreConnectHook:0x101c74190 @hooks=[]>, @open_timeout=nil, @log=nil, @keep_alive_time=300, @proxy_pass=nil, @redirect_ok=true, @post_connect_hook=#<WWW::Mechanize::Chain::PostConnectHook:0x101c74168 @hooks=[]>, @conditional_requests=true, @password=nil, @cert=nil, @user_agent="WWW-Mechanize/0.9.3 (http://rubyforge.org/projects/mechanize/)", @pluggable_parser=#<WWW::Mechanize::PluggableParser:0x101c74550 @default=WWW::Mechanize::File, @parsers={"application/xhtml+xml"=>WWW::Mechanize::Page, "text/html"=>WWW::Mechanize::Page, "application/vnd.wap.xhtml+xml"=>WWW::Mechanize::Page}>, @verify_callback=nil, @connection_cache={}, @proxy_user=nil, @pass=nil, @ca_file=nil, @request_headers={}, @user=nil, @cookie_jar=#<WWW::Mechanize::CookieJar:0x101c746b8 @jar={}>, @scheme_handlers={"https"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>, "file"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>, "http"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>, "relative"=>#<Proc:0x00000001020c12c0@/Library/Ruby/Gems/1.8/gems/mechanize-0.9.3/lib/www/mechanize.rb:152>}, @redirection_limit=20, @proxy_port=nil, @history_added=nil, @auth_hash={}, @read_timeout=nil, @keep_alive=true, @history=[], @key=nil>
使用agent我们就可以登陆Ta-da list . 要解决这个,我们需要获取登陆页面,输入密码然后提交表单.
通过调用agent.get,并传入页面的URL, 发送一个GET请求来获取页面内容
``` terminal >> agent.get("http://asciicasts.tadalist.com/session/new") => #<p>这里返回的是一个<code>Mechanize::Page</code>对象,它包含这个页面里所有元素内容. 对于我们这里的页面,需要的是登陆表单.</p> <p>任何时候调用<code>agent.page</code>都会返回当前page对象,可以通过调用页面属性来访问页面上的不同元素. 例如,要得到页面上的表单元素,可以调用<code>agent.page.forms</code>,它返回的是一个<code>Mechanize::Form</code>对象数组. 由于这个页面只有一个表单,所以调用<code>agent.page.forms.first</code>就可以索引到我们需要的登陆表单. 后面要用到这个表单,所以先将该表单标记为一个变量.</p> ``` terminal > > form = agent.page.forms.first => #<WWW::Mechanize::Form {name nil} {method "POST"} {action "/session"} {fields #<WWW::Mechanize::Form::Field:0x1035f1708 @name="username", @value="asciicasts"> #<WWW::Mechanize::Form::Field:0x1035ef4a8 @name="password", @value="">} {radiobuttons} {checkboxes #<WWW::Mechanize::Form::CheckBox:0x1035eeb48 @checked=false, @name="save_login", @value="1">} {file_uploads} {buttons}>
通过上面输出form的fields
集合,我们发现用户名已经被填写,密码却为空. 在这里可以通过为Ruby对象设置属性来完成表单的填写.下面是设置密码:
<p>提交这个表单是相当简单, 唯一需要做的是调用<code>form.submit</code>. 它将返回另外一个<code>Mechanize::Page</code>对象.</p> ``` terminal >> form.submit => #<WWW::Mechanize::Page {url #<URI::HTTP:0x10336ad68 URL:http://asciicasts.tadalist.com/lists>} {meta} {title "My Ta-da Lists"} {iframes} {frames} {links #<WWW::Mechanize::Page::Link "Highrise" "http://www.highrisehq.com"> #<WWW::Mechanize::Page::Link "Try it free" "http://www.highrisehq.com"> #<WWW::Mechanize::Page::Link "Tada-mark-bg" "http://asciicasts.tadalist.com/lists"> #<WWW::Mechanize::Page::Link "Create a new list" "/lists/new"> #<WWW::Mechanize::Page::Link "Wish List" "/lists/1463636"> #<WWW::Mechanize::Page::Link "Rss" "http://asciicasts.tadalist.com/lists.rss?token=8ee4a563af677d3ebf3ceb618dac600a"> #<WWW::Mechanize::Page::Link "Log out" "/session"> #<WWW::Mechanize::Page::Link "change password" "/account/change_password"> #<WWW::Mechanize::Page::Link "change email" "/account/change_email_address"> #<WWW::Mechanize::Page::Link "cancel account" "/account/destroy"> #<WWW::Mechanize::Page::Link "FAQs" "http://www.tadalist.com/help"> #<WWW::Mechanize::Page::Link "Terms of Service" "http://www.tadalist.com/terms"> #<WWW::Mechanize::Page::Link "Privacy Policy" "http://www.tadalist.com/privacy"> #<WWW::Mechanize::Page::Link "other products from 37signals" "http://www.37signals.com">} {forms}>
上面就是这个页面的内容,显示了我们的清单,接下来需要做的就是点击链接去到商品列表页面. 下面是浏览器中的对应页面. 当使用Mechanize时,它可以帮助我们模拟浏览器以便你决定下一步执行什么脚本.
要获取清单列表,我们需要点击"Wish List"链接. 但是页面上有很多链接,怎样找到Mechanize要点击的链接呢? 可以通过agent.page.links获得页面的所有链接,然后进行迭代,循环每个链接的text
属性,找到我们需要的.另外有一个更容易的办法就是通过 link_with
:
<p>使用<code>link_with</code>方法可以返回一个匹配指定条件的链接,这样就可以获取带有"Wish List"文本的链接. 表单也有类似的方法<code>form_with</code>. 还有匹配多个对象的方法 ,<code>links_with</code> 和<code>forms_with</code>是用来匹配指定条件的多个链接或多个表单.</p> <p>既然已经找到了需要的链接,我们就可以点击它,它会定向到清单列表页面.</p> ``` terminal agent.page.link_with(:text => "Wish List").click => #<WWW::Mechanize::Page {url #<URI::HTTP:0x103261138 URL:http://asciicasts.tadalist.com/lists/1463636>}
准备工作已经完成,我们已经找到了想要抓取内容的页面. 现在可以使用Nokogiri来提取内容了.但是首先还需要获得匹配列表项的CSS选择器 跟上次一样,我们需要用SelectorGadget来获取对应的选择器.
点击清单的第一项,会选中第一个item,当点击下一个时,所有的清单项都被选中了,于是找到了需要的选择器.edit_item
.
使用Nokogiri,可以调用page对象的两个方法来提取页面元素.第一个是at
,它返回匹配对应选择器的一个元素.
<p>第二个是<code>search</code>. 类似地,它返回匹配到的所有元素的数组.</p> ``` ruby agent.page.search(".edit_item")
在列表中有一些items,因此需要使用第二个方法. 使用上面的命令将返回一个Nokogiri::XML::Element对象数组,每一个元素代表清单中的一个列表项.我们可以通过控制输出来让结果具有可读性.
``` terminal >> agent.page.search(".edit_item").map(&:text).map(&:strip) => ["Settler's of Catan", "Go for Beginners book", "Nintendo DSi", "Chess Set", "Dark Knight on Blu Ray", "Modern Warfare 2 for Xbox", "Scrabble", "Dragon Age Strategy Guide", "Wario Land: Shake It!"]<p>获取每个元素的<code>text</code>属性,并调用strip方法来去掉空白部分.就可以获得这些列表项名字的数组,这刚好是我们需要的.</p> <h3>集成Mechanize到Rails应用</h3> <p>知道了如何使用Mechanize,现在就可以将刚才的代码集成到Rails应用里. 我们将使用上集使用过的shop应用.</p> <div class="imageWrapper"> <img src="http://railscasts.com/static/episodes/asciicasts/E191I05.png" width="808" height="371" alt="Our application's product list."/> </div> <p>跟抓取价格相反,这次我们需要从Ta-da list导入我们的新商品.可以在<code>/lib/tasks/product_prices.rake</code>里创建一个rake任务来处理这个.但是我们该怎么写代码呢?接下来从console开始,然后复制里面的代码.</p> <p>但是从console里面复制代码是有些困难,因为它是每一行复合输出的. 可以用下面的命令来返回我们之前的所有输入.</p> ``` terminal >> puts Readline::HISTORY.entries.split("exit").last[0..-2].join("\n") require 'mechanize' agent = WWW::Mechanize.new agent.get("http://asciicasts.tadalist.com/session/new") form = agent.page.forms.first form.password = "password" form.submit agent.page.link_with(:text => "Wish List").click agent.page.search(".edit_item").map(&:text).map(&:strip) => nil
上面已经列出了需要复制到rake里面的代码. 现在我们清理一下代码,然后去循环提取到的商品,为每一个创建一个Product
.
<p>当然,可以去掉用户名和密码,通过控制参数传入它们. 现在我们需要切换窗口,看看我们的rake任务能不能正常工作.</p> ``` terminal $ rake import_list (in /Users/eifion/rails/apps_for_asciicasts/ep191/shop)
如果运行脚步后,没有异常,就可以刷新products页面了.
脚步已经工作了: 现在已经为列表中的每一个商品创建了一个Product. 如果我们运行上集中的rake任务,我们就可以获得所有新商品的价格.
到目前为止,所有的工作都已经完成了. 我们已经通过Mechanize和Nokogiri来在页面间导航,填写表单进行页面交互,点击超链接获取我们想要的信息. 对于网站的数据抓取工作,这是一个非常不错的解决办法.