关于python:如何只复制已经存在的目标文件上更改的文件内容?

How to copy only the changed file-contents on the already existed destination file?

我有一个脚本,用于从一个位置复制到另一个位置,目录结构下面的文件都是.txt文件。

此脚本只计算源文件的大小,并且仅在文件大小不是零字节时进行复制。但是,我需要在一个cron中运行这个脚本,在一定的时间间隔后复制任何增加的数据。

因此,我需要知道如何只复制源文件上更新的文件内容,然后只更新新内容的目标,而不只是覆盖目标中已经存在的内容。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/python3
import os
import glob
import shutil
import datetime

def Copy_Logs():
    Info_month = datetime.datetime.now().strftime("%B")
    # The result of the below glob _is_ a full path
    for filename in glob.glob("/data1/logs/{0}/*/*.txt".format(Info_month)):
        if os.path.getsize(filename) > 0:
            if not os.path.exists("/data2/logs/" + os.path.basename(filename)):
                shutil.copy(filename,"/data2/logs/")

if __name__ == '__main__':
    Copy_Logs()

我正在寻找是否有办法使用shutil()来实现rsync的工作方式,或者是否有其他方法来实现我所拥有的代码。

简而言之,如果还没有复制,我只需要复制一个文件,如果源代码得到更新,那么只需要复制增量文件。

注:Info_month = datetime.datetime.now().strftime("%B")是强制保存的,因为它按月份名称确定当前目录。

编辑:

如果我们可以使用filecmpshutil.copyfile模块来比较文件和目录,但我不知道如何将其放入代码中,那就另当别论了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
import glob
import filecmp
import shutil
import datetime

def Copy_Logs():
    Info_month = datetime.datetime.now().strftime("%B")
    for filename in glob.glob("/data1/logs/{0}/*/*.txt".format(Info_month)):
        if os.path.getsize(filename) > 0:
            if not os.path.exists("/data2/logs/" + os.path.basename(filename)) or not filecmp.cmp("/data2/logs/" + os.path.basename(filename),"/data2/logs/"):
                shutil.copyfile(filename,"/data2/logs/")

if __name__ == '__main__':
    Copy_Logs()


您可以使用Google的diff-match补丁(您可以将其与pip install diff-match-patch一起安装)来创建diff并从中应用补丁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import diff_match_patch as dmp_module

#...
if not os.path.exists("/data2/logs/" + os.path.basename(filename)):
    shutil.copy(filename,"/data2/logs/")
else:
    with open(filename) as src, open("/data2/logs/" + os.path.basename(filename),
                                                                        'r+') as dst:
        dmp = dmp_module.diff_match_patch()

        src_text = src.read()
        dst_text = dst.read()

        diff = dmp.diff_main(dst_text, src_text)

        if len(diff) == 1 and diff[0][0] == 0:
            # No changes
            continue

        #make patch
        patch = dmp.patch_make(dst_text, diff)
        #apply it
        result = dmp.patch_apply(patch, dst_text)

        #write
        dst.seek(0)
        dst.write(result[0])
        dst.truncate()


如前所述,在需要执行增量文件列表或数据增量的情况下,rsync是一种更好的工作方式,因此,我宁愿一直使用rsync和subprocess模块。

但是,您也可以指定一个变量Curr_date_month,以获取当前日期、月份和年份,作为您只需从当前月份和日期文件夹复制文件的要求。此外,还可以定义源变量和目标变量,以便于将它们写入代码中。

其次,虽然您可以使用getsize检查文件大小,但我想添加rsync选项参数--min-size=,以确保不复制零字节文件。

这里是您的最终代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/python3
import os
import glob
import datetime
import subprocess

def Copy_Logs():
    # Variable Declaration to get the month and Curr_date_month
    Info_month = datetime.datetime.now().strftime("%B")
    Curr_date_month = datetime.datetime.now().strftime("%b_%d_%y")
    Sourcedir ="/data1/logs"
    Destdir ="/data2/logs/"
    ###### End of your variable section #######################
    # The result of the below glob _is_ a full path
    for filename in glob.glob("{2}/{0}/{1}/*.txt".format(Info_month, Curr_date_month, Sourcedir)):
        if os.path.getsize(filename) > 0:
            if not os.path.exists(Destdir + os.path.basename(filename)):
                subprocess.call(['rsync', '-avz', '--min-size=1', filename, Destdir ])

if __name__ == '__main__':
    Copy_Logs()


rsync的好处之一是它只复制文件之间的差异。当文件变得巨大时,它会大大减少I/O。

pypi中的原始程序周围有过多的类rsync的实现和包装。这个博客文章描述了如何以非常好的方式在Python中实现rsync,并且可以按原样使用。

至于检查是否需要进行同步,您可以使用filecmp.cmp()。在它的浅变体中,它只检查os.stat()签名。


这方面有一些非常有趣的想法,但我会尝试提出一些新的想法。

想法1:追踪更新的更好方法

根据您的问题,很明显您正在使用cron作业来跟踪更新的文件。

如果您试图监视的文件/目录数量相对较少,我建议使用另一种方法来简化您的生活。

您可以使用Linux inotify机制,它允许您监视特定的文件/目录,并在文件写入时得到通知。

pro:您可以立即知道每一次写入,而无需检查更改。当然,您可以编写一个处理程序,它不会每次写入都更新目标,只会在X分钟内更新一个。

下面是一个使用inotifypython包的示例(取自包的页面):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import inotify.adapters

def _main():
    i = inotify.adapters.Inotify()

    i.add_watch('/tmp')

    with open('/tmp/test_file', 'w'):
        pass

    for event in i.event_gen(yield_nones=False):
        (_, type_names, path, filename) = event

        print("PATH=[{}] FILENAME=[{}] EVENT_TYPES={}".format(
              path, filename, type_names))

if __name__ == '__main__':
    _main()

想法2:只复制更改

如果您决定使用inotify机制,那么跟踪您的状态将是微不足道的。

然后,有两种可能性:

1。总是追加新内容

如果是这种情况,您可以简单地从最后一个偏移量复制任何内容,直到文件结束。

2。新内容在随机位置写入

在这种情况下,我建议使用其他答案提出的方法:使用diff补丁。在我看来,这是迄今为止最优雅的解决方案。

以下是一些选项:

  • 差异匹配修补程序
  • 微分与补片


一种方法是将一行保存到一个文件中,以跟踪(在os.path.getctime的帮助下)您复制文件的最新时间,并在每次复制时维护该行。

注意:可以优化以下代码段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import datetime
import glob
import os
import shutil

Info_month = datetime.datetime.now().strftime("%B")
list_of_files = sorted(glob.iglob("/data1/logs/{0}/*/*.txt".format(Info_month)), key=os.path.getctime, reverse=True)
if not os.path.exists("track_modifications.txt"):
    latest_file_modified_time = os.path.getctime(list_of_files[0])
    for filename in list_of_files:
            shutil.copy(filename,"/data2/logs/")
    with open('track_modifications.txt', 'w') as the_file:
        the_file.write(str(latest_file_modified_time))
else:
    with open('track_modifications.txt', 'r') as the_file:
        latest_file_modified_time = the_file.readline()
    should_copy_files = [filename for filename in list_of_files if
                         os.path.getctime(filename) > float(latest_file_modified_time)]
    for filename in should_copy_files:
            shutil.copy(filename,"/data2/logs/")

方法是,创建一个文件,其中包含系统修改的最新文件的时间戳。

检索所有文件并按修改时间排序

1
list_of_files = sorted(glob.iglob('directory/*.txt'), key=os.path.getctime, reverse=True)

最初,在if not os.path.exists("track_modifications.txt"):中,我检查该文件是否存在(即首次复制),然后将最大的文件时间戳保存到

1
latest_file_modified_time = os.path.getctime(list_of_files[0])

我只是复制所有给定的文件,并将这个时间戳写到track_modifications文件中。

否则,文件就存在了(即以前复制过文件),我只需读取时间戳并将其与我在list_of_files中读取的文件列表进行比较,然后检索时间戳较大的所有文件(即,在上次复制的文件之后创建的文件)。那是在

1
should_copy_files = [filename for filename in list_of_files if os.path.getctime(filename) > float(latest_file_modified_time)]

实际上,跟踪最新修改的文件的时间戳还可以让您复制更改时已复制的文件:)


您需要将更改保存到某个地方,或者在文件内容更改时侦听事件。对于后者,您可以使用watchdog

如果您决定真正喜欢cron而不是增量检查更改(看门狗),那么您需要将更改存储在一些数据库中。一些基本的例子是:

1
2
3
ID | path        | state before cron
1  | /myfile.txt | hello
...| ...         | ...

然后,为了检查diff,您将cron之前的状态转储到一个文件,运行一个简单的diff old.txt new.txt,如果有一些输出(即有更改),您将复制整个文件或仅复制diff的输出,然后将其作为patch应用到要覆盖的文件。

如果没有diff输出,则文件中没有更改,因此没有要更新的内容。

编辑:实际上:d如果文件在同一台计算机上,您甚至可能不需要数据库…这样,您就可以直接在新文件和旧文件之间进行diff+修补。

例子:

1
2
3
4
5
6
7
8
$ echo 'hello' > old.txt && echo 'hello' > new.txt
$ diff old.txt new.txt                             # empty
$ echo 'how are you' >> new.txt                    # your file changed
$ diff old.txt new.txt > my.patch && cat my.patch  # diff is not empty now
1a2
> how are you

$ patch old.txt < my.patch  # apply the changes to the old file

在具有相同old.txtnew.txt基的python中:

1
2
3
from subprocess import Popen, PIPE
diff = Popen(['diff', 'old.txt', 'new.txt']).communicate()[0]
Popen(['patch', 'old.txt'], stdin=PIPE).communicate(input=diff)


您必须集成一个数据库,并且可以根据文件的大小、名称和作者来记录文件。

如果有任何更新,文件的大小将发生变化,您可以相应地更新或追加。