一次批量修改博客文章的经验(下):操作过程
上一篇文章中我們進行了一些預備工作,主要是了解了該如何使用MetaWeblog API讀取和修改博客園的文章——包括同步和異步兩種調(diào)用方式。此外,由于F#在異步調(diào)用方面的優(yōu)勢,我決定使用F#來完成批量修改文章任務。這個任務并不困難,但很“危險”,一旦出錯可能之前的文章就無法恢復了。因此,我把這個任務拆成多個步驟,每個步驟都會將數(shù)據(jù)保存在硬盤上。由此,即便出錯,還是有挽回的余地。
獲取所有文章ID
首先,我們便要下載所有文章了,這又該怎么做呢?雖然MetaWeblog API提供了getRecentPosts方法用來獲取最近的文章,但是這個接口卻并不好用。例如,它只能用來獲取最新的幾篇文章內(nèi)容,但對我來說,我想修改的其實是很久之前的文章。那么,難道要我下載全部500多篇文章才行嗎?后來我統(tǒng)計了一下,所有文章大小存成文本文件大約有10M,一個請求下載10M內(nèi)容還是有些夸張的——而且還看不到進度。因此,我最后打算“曲線救國”,先著手獲得所有公開文章的ID,再通過getPost接口獲得文章內(nèi)容。
MetaWeblog API并不提供獲取所有文章ID的接口,但這并不影響我們從網(wǎng)頁上直接進行抓取。我們從博客園提供的“月份匯總”頁面入手,即這樣的一張頁面。博客園的月份匯總的URL非常有規(guī)律,獲得它的HTML內(nèi)容之后即可使用正則表達式來捕獲文章ID了。您可能會想,一篇文章可能會取別名(這樣URL上就不顯示ID了),而網(wǎng)頁上各種URL也很多,有什么辦法可以準確而方便地分析出文章ID嗎?其實這個問題很簡單,因為博客園為每篇文章都放置了一個“編輯”鏈接,它的URL是.../EditPosts.aspx?postid=1633416,對我們來說再方便不過了。
于是下載和捕獲文章ID的方法可謂手到擒來:
type WebClient with member c.GetStringAsync(url) =async {let completeEvent = c.DownloadStringCompleteddo 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.Monthlet! 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.Parallellists|> List.concat|> List.sort|> List.map (fun i -> i.ToString())|> fun lines -> File.WriteAllLines("postIds.txt", lines)}下載文章ID的任務由downloadPostIdsAsync函數(shù)完成,它接受beginMonth和endMonth兩個DateTime參數(shù)來表示月份的區(qū)間,我們將從中獲取所有的文章ID。downloadPostIdsAsync函數(shù)會生成一個異步工作流,執(zhí)行這個工作流便會將所有的文章ID進行排序,并保存至postIds.txt文件中去,一行一個。獲取單月的文章ID由內(nèi)部的downloadPostIdsAsync'這個輔助函數(shù)負責,它會構(gòu)造出下載單個月份文章ID的異步工作流,再由外部函數(shù)合并而成——換句話說,所有月份的文章將同時進行異步下載,提高效率。
我們可以在main方法中執(zhí)行downloadPostIdsAsync函數(shù),這樣postIds.txt文件中便會出現(xiàn)所有的文章ID了:
System.Net.ServicePointManager.DefaultConnectionLimit <- 10Blogging.downloadPostIdsAsync (new DateTime(2006, 9, 1)) (new DateTime(2008, 12, 1)) |> Async.RunSynchronously由于WebClient基于WebRequest對象實現(xiàn),而WebRequest受到ServicePointManager控制,因此我們要設置其DefaultConnectionLimit屬性來打開對單個域名的限制——并控制對并發(fā)連接的數(shù)量進行限制,以免對服務器產(chǎn)生太大壓力(當然我們其實工作量本不大,且博客園也不會那么脆弱)。當然,這個限制也可以通過配置進行更改。
下載所有文章
這個任務要比之前簡單許多,因為MetaWeblog API已經(jīng)提供了可以直接使用的接口。
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 <- apiUrllet! post = proxy.GetPostAsync(id, userName, password)let file = sprintf @"posts\%i.xml" post.PostIDlet xml = XmlSerialization.serialize postFile.WriteAllText(file, xml);printfn "post %i downloaded" post.PostID}File.ReadAllLines("postIds.txt")//|> Seq.take 10|> Seq.map downloadPostAsync|> Async.Parallel|> Async.Ignore調(diào)用:
Blogging.downloadPostsAsync() |> Async.RunSynchronously與之前一樣,所有文章都同時下載,并使用之前討論過的XML序列化方式保存在磁盤上。執(zhí)行downloadPostsAsync函數(shù)時,控制臺上會陸陸續(xù)續(xù)地打印出文章下載完成的字樣。這也是我為什么不希望使用getRecentPosts接口獲取文章的原因:有進度,有盼頭。
修改及提交文章
對于此類修改任務,最直接的方法還是使用正則表達式。首先,我們?nèi)』厮械南螺d好的文章,篩選出所有需要修改的那些,再將修改后的內(nèi)容另存為新的文件:
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.PostIDlet xml = XmlSerialization.serialize postFile.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提交文章的過程,它只不過是讀回所有文章信息,再調(diào)用我們之前準備的代理而已:
let updateRemotePosts() = let updateFromFile file = async {let proxy = MetaWeblog.createProxy()do proxy.Url <- apiUrllet post = File.ReadAllText(file) |> XmlSerialization.deserialize<MetaWeblog.Post> xmllet! 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調(diào)用:
Blogging.updateLocalPosts()Blogging.updateRemotePostsAsync() |> Async.RunSynchronously如此,我們的批量更新任務就完成了。
總結(jié)
除了“去除段首空格”之外,我還做了其他一些修改,例如調(diào)整以前不太好的代碼粘貼方式等等,這些只需要對代碼進行簡單修改就行了。在實際執(zhí)行任務時,我也并非一蹴而就,也使用了少數(shù)幾篇文章進行試驗,確定沒有問題之后才作了批量提交。總體來說,整個過程沒有遇到什么麻煩,現(xiàn)在那些舊文章的格式終于也得到了一定程度的清理。
可惜的是,我最早的幾十篇文章中,分段簡直是亂來的,一會兒是div,一會兒又用<br />,甚至連用多個……真是只要“所見即所得”就不顧一切了。對于這種問題,有機會再想辦法做調(diào)整吧。
本文代碼
相關(guān)文章
- 一次批量修改博客文章的經(jīng)驗(上):準備工作
- 一次批量修改博客文章的經(jīng)驗(下):操作過程
總結(jié)
以上是生活随笔為你收集整理的一次批量修改博客文章的经验(下):操作过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分期乐为什么有额度借不出
- 下一篇: c#解压,压缩文件!!!