利用Google快照,恢复误删的WordPress数据库 

起因:前些天试图修复MySQL经常性崩溃的问题,结果误删WordPress数据库(误删ibdata文件后重启了MySQL进程,失去了恢复机会)。由于之前从未做过备份,本博客所有文章全部丢失。但Google默默地为我的网站作了快照,所以仍然有机会从Google快照中恢复网站内容。

抓取快照网址

1. 将Google搜索每页搜索结果数设为最大值100

2. 使用Google搜索site:jinzihao.me

3. 对于每页搜索结果(“for page in search_results:”),在浏览器控制台中执行以下代码:

function saveData(data, filename) {
    var a = document.createElement('a');
    var url = window.URL.createObjectURL(data);
    a.href = url;
    a.download = filename;
    a.click();
    window.URL.revokeObjectURL(url);
}

var regex = /"(https:\/\/webcache.googleusercontent.com\/.*?)"/g;
var urls = [];
match = regex.exec(document.body.innerHTML);
while (match != null) {
    urls.push(match[1] + '&vwsrc=1');
    match = regex.exec(document.body.innerHTML);
}
saveData(new Blob([urls.join("\n")]), 'url.txt');

上述代码提取网页中所有快照链接(https://webcache.googleusercontent.com/……),将其保存到文件url.txt。添加”vwsrc=1″参数使Google快照显示网页原始的HTML代码,便于之后重建网站内容。

由于搜索结果页数不多,本人采用了手动翻页的方式来抓取快照链接。之后将几个网址列表(url.txt)合并到一个文件,这就是下一步抓取的目标列表了。

图1. 抓取到的快照网址列表

抓取快照内容

1. 打开一个Google快照页面,确保其在https://webcache.googleusercontent.com域名下,这样我们才能通过AJAX请求抓取其他快照页面。

2. 将前一步抓取的快照网址列表放置在服务器上,并在服务器上设置好CORS策略,这样我们可以通过AJAX请求获取到该列表。

3. 在浏览器控制台中执行以下代码:

function saveData(data, filename) {
    var a = document.createElement('a');
    var url = window.URL.createObjectURL(data);
    a.href = url;
    a.download = filename;
    a.click();
    window.URL.revokeObjectURL(url);
}

xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", "https://lab.jinzihao.me/cors/urllist.txt", false);
xmlhttp.send();
urls = xmlhttp.responseText.split("\n");
for (var url of urls) {
    console.log(url);
    xmlhttp = new XMLHttpRequest();
    xmlhttp.open("GET", url, false);
    xmlhttp.send();
    url = url.split("+")[0].split(":")[4];
    console.log(url);
    saveData(new Blob([xmlhttp.responseText]), "jinzihao_me_" + btoa(url));
}

这样就将网站在Google快照中的所有页面抓取了下来,其中文件名是jinzihao_me_ + base64编码的原始网址,文件内容则是Google快照页面的全部HTML代码。

图2. 抓取到的快照页面

重建网站

首先清空wp_posts表,确保文章ID不会产生冲突:

truncate table wp_posts;

接下来使用如下Python程序,遍历上一步抓取到的快照页面,并将其插回到MySQL数据库,重建wp_posts表(由于MySQLdb模块只支持Python 2,本代码只能运行在Python 2上):

#!/usr/bin/env python
#!-*- coding: UTF-8 -*-
import os
import sys
import html
import HTMLParser
from bs4 import BeautifulSoup
import time
import MySQLdb

os.environ['TZ'] = 'Asia/Chongqing'
time.tzset()

result = dict()

db = MySQLdb.connect('localhost', '****username****', '****password****', '****dbname****', charset='utf8')
cursor = db.cursor()

for filename in os.listdir(sys.argv[1]):
    print("File: " + filename)
    # f = open(os.path.join(sys.argv[1], filename), 'r', encoding='utf-8')
    f = open(os.path.join(sys.argv[1], filename), 'r')
    content = f.read()
    f.close()
    soup = BeautifulSoup(content, 'html.parser')
    # inner_content = html.unescape(soup.find_all('pre')[0].decode_contents())
    inner_content = HTMLParser.HTMLParser().unescape(soup.find_all('pre')[0].decode_contents())
    inner_soup = BeautifulSoup(inner_content, 'html.parser')
    permalinks = []
    gmt_times = []
    local_times = []
    post_types = []
    metas = inner_soup.select('span.meta')
    for meta in metas:
        if len(meta.a['href'].split('#')) == 1:
            if meta.abbr is None:
                post_types.append('page')
                post_time = '1970-01-01 08:00:00'
            else:
                post_types.append('post')
                post_time = meta.abbr['title']
            gmt_times.append(post_time[:10] + ' ' + post_time[11:19])
            local_times.append(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(time.mktime(time.strptime(gmt_times[-1], '%Y-%m-%d %H:%M:%S')) + 57600)))
            permalinks.append(meta.a['href'])


    posts = inner_soup.select('div.postcontent')

    for id, post in enumerate(posts):
        post_id = int(post['id'].split('-')[1])
        if post.h2 is None:
            post_title = ''
            post_content = post.decode_contents()
        elif post.h2.a is None:
            post_title = post.h2.decode_contents()
            post_content = post.decode_contents()
            post_content = post_content[post_content.find('</h2>') + 5:]
        else:
            post_title = post.h2.a.decode_contents()
            post_content = post.decode_contents()
            post_content = post_content[post_content.find('</h2>') + 5:]
        if (post_id not in result) or (len(post_content) > len(result[post_id][1])):
            result[post_id] = [post_title, post_content, gmt_times[id], local_times[id], permalinks[id], permalinks[id].split('/')[-2], post_types[id]]

for id, data in result.items():
    sql = "INSERT INTO wp_posts (ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
    cursor.execute(sql, (id, 1, data[3], data[2], data[1], data[0], '', 'publish', 'open', 'open', '', data[5], '', '', data[3], data[2], '', 0, data[4], 0, data[6], '', 0))

db.commit()
db.close()

这段代码使用Beautiful Soup对Google快照页面做了解析,提取最外层的<pre>元素,其中包含被快照的网页的HTML代码。对其进行一次HTML反转义,再次用Beautiful Soup进行解析,提取其中的<span class=”meta”>,获取到文章的元信息(时间和网址);提取其中的<div class=”postcontent”>,获取到文章ID和内容。最后将其插入到wp_posts表,即可重建WordPress文章数据。