一次批量修改博客文章的经验(下):操作过程
2010-01-05 19:40 Jeffrey Zhao 阅读(5572) 评论(8) 编辑 收藏 举报上一篇文章中我们进行了一些预备工作,主要是了解了该如何使用MetaWeblog API读取和修改博客园的文章——包括同步和异步两种调用方式。此外,由于F#在异步调用方面的优势,我决定使用F#来完成批量修改文章任务。这个任务并不困难,但很“危险”,一旦出错可能之前的文章就无法恢复了。因此,我把这个任务拆成多个步骤,每个步骤都会将数据保存在硬盘上。由此,即便出错,还是有挽回的余地。
获取所有文章ID
首先,我们便要下载所有文章了,这又该怎么做呢?虽然MetaWeblog API提供了getRecentPosts方法用来获取最近的文章,但是这个接口却并不好用。例如,它只能用来获取最新的几篇文章内容,但对我来说,我想修改的其实是很久之前的文章。那么,难道要我下载全部500多篇文章才行吗?后来我统计了一下,所有文章大小存成文本文件大约有10M,一个请求下载10M内容还是有些夸张的——而且还看不到进度。因此,我最后打算“曲线救国”,先着手获得所有公开文章的ID,再通过getPost接口获得文章内容。
MetaWeblog API并不提供获取所有文章ID的接口,但这并不影响我们从网页上直接进行抓取。我们从博客园提供的“月份汇总”页面入手,即这样的一张页面。博客园的月份汇总的URL非常有规律,获得它的HTML内容之后即可使用正则表达式来捕获文章ID了。您可能会想,一篇文章可能会取别名(这样URL上就不显示ID了),而网页上各种URL也很多,有什么办法可以准确而方便地分析出文章ID吗?其实这个问题很简单,因为博客园为每篇文章都放置了一个“编辑”链接,它的URL是.../EditPosts.aspx?postid=1633416,对我们来说再方便不过了。
于是下载和捕获文章ID的方法可谓手到擒来:
type WebClient with member c.GetStringAsync(url) = async { let completeEvent = c.DownloadStringCompleted do c.DownloadStringAsync(new Uri(url)) let! args = Async.AwaitEvent(completeEvent) return args.Result } let downloadPostIdsAsync (beginMonth : DateTime) (endMonth : DateTime) = let downloadPostIdsAsync' (m : DateTime) = async { let webClient = new WebClient() let url = sprintf "http://www.cnblogs.com/JeffreyZhao/archive/%i/%i.html" m.Year m.Month let! html = webClient.GetStringAsync(url) let regex = @"EditPosts\.aspx\?postid=(\d+)" return [ for m in Regex.Matches(html, regex) -> m.Groups.Item(1).Value |> Int32.Parse ] } async { let! lists = Seq.initInfinite (fun i -> beginMonth.AddMonths(i)) |> Seq.takeWhile (fun m -> m <= endMonth) |> Seq.map downloadPostIdsAsync' |> Async.Parallel lists |> List.concat |> List.sort |> List.map (fun i -> i.ToString()) |> fun lines -> File.WriteAllLines("postIds.txt", lines) }
下载文章ID的任务由downloadPostIdsAsync函数完成,它接受beginMonth和endMonth两个DateTime参数来表示月份的区间,我们将从中获取所有的文章ID。downloadPostIdsAsync函数会生成一个异步工作流,执行这个工作流便会将所有的文章ID进行排序,并保存至postIds.txt文件中去,一行一个。获取单月的文章ID由内部的downloadPostIdsAsync'这个辅助函数负责,它会构造出下载单个月份文章ID的异步工作流,再由外部函数合并而成——换句话说,所有月份的文章将同时进行异步下载,提高效率。
我们可以在main方法中执行downloadPostIdsAsync函数,这样postIds.txt文件中便会出现所有的文章ID了:
System.Net.ServicePointManager.DefaultConnectionLimit <- 10 Blogging.downloadPostIdsAsync (new DateTime(2006, 9, 1)) (new DateTime(2008, 12, 1)) |> Async.RunSynchronously
由于WebClient基于WebRequest对象实现,而WebRequest受到ServicePointManager控制,因此我们要设置其DefaultConnectionLimit属性来打开对单个域名的限制——并控制对并发连接的数量进行限制,以免对服务器产生太大压力(当然我们其实工作量本不大,且博客园也不会那么脆弱)。当然,这个限制也可以通过配置进行更改。
下载所有文章
这个任务要比之前简单许多,因为MetaWeblog API已经提供了可以直接使用的接口。
let apiUrl = "http://www.cnblogs.com/JeffreyZhao/services/metaweblog.aspx" let userName, password = "JeffreyZhao", "..." let downloadPostsAsync() = let downloadPostAsync id = async { let proxy = MetaWeblog.createProxy() do proxy.Url <- apiUrl let! post = proxy.GetPostAsync(id, userName, password) let file = sprintf @"posts\%i.xml" post.PostID let xml = XmlSerialization.serialize post File.WriteAllText(file, xml); printfn "post %i downloaded" post.PostID } File.ReadAllLines("postIds.txt") //|> Seq.take 10 |> Seq.map downloadPostAsync |> Async.Parallel |> Async.Ignore
调用:
Blogging.downloadPostsAsync() |> Async.RunSynchronously
与之前一样,所有文章都同时下载,并使用之前讨论过的XML序列化方式保存在磁盘上。执行downloadPostsAsync函数时,控制台上会陆陆续续地打印出文章下载完成的字样。这也是我为什么不希望使用getRecentPosts接口获取文章的原因:有进度,有盼头。
修改及提交文章
对于此类修改任务,最直接的方法还是使用正则表达式。首先,我们取回所有的下载好的文章,筛选出所有需要修改的那些,再将修改后的内容另存为新的文件:
let updateLocalPosts() = let regex = @"(?i)(<p\b[^>]*>) ? ?" let target = "$1" let updatePost (post : MetaWeblog.Post) = post.Content <- Regex.Replace(post.Content, regex, target) let file = sprintf @"updated\%i.xml" post.PostID let xml = XmlSerialization.serialize post File.WriteAllText(file, xml); Directory.GetFiles(@"posts\", "*.xml") |> Seq.map (fun f -> File.ReadAllText(f)) |> Seq.map XmlSerialization.deserialize<MetaWeblog.Post> |> Seq.filter (fun p -> Regex.IsMatch(p.Content, regex)) |> Seq.iter updatePost
最后则是重新使用MetaWeblog API提交文章的过程,它只不过是读回所有文章信息,再调用我们之前准备的代理而已:
let updateRemotePosts() = let updateFromFile file = async { let proxy = MetaWeblog.createProxy() do proxy.Url <- apiUrl let post = File.ReadAllText(file) |> XmlSerialization.deserialize<MetaWeblog.Post> xml let! result = proxy.UpdatePostAsync(post.PostID.ToString(), userName, password, post, true) File.Delete(file) printfn "post %i updated" post.PostID } Directory.GetFiles(@"updated\", "*.xml") |> Seq.map updateFromFile |> Async.Parallel |> Async.Ignore
调用:
Blogging.updateLocalPosts() Blogging.updateRemotePostsAsync() |> Async.RunSynchronously
如此,我们的批量更新任务就完成了。
总结
除了“去除段首空格”之外,我还做了其他一些修改,例如调整以前不太好的代码粘贴方式等等,这些只需要对代码进行简单修改就行了。在实际执行任务时,我也并非一蹴而就,也使用了少数几篇文章进行试验,确定没有问题之后才作了批量提交。总体来说,整个过程没有遇到什么麻烦,现在那些旧文章的格式终于也得到了一定程度的清理。
可惜的是,我最早的几十篇文章中,分段简直是乱来的,一会儿是div,一会儿又用<br />,甚至连用多个……真是只要“所见即所得”就不顾一切了。对于这种问题,有机会再想办法做调整吧。
相关文章
- 一次批量修改博客文章的经验(上):准备工作
- 一次批量修改博客文章的经验(下):操作过程