Skip to content

Latest commit

 

History

History
1255 lines (884 loc) · 51.1 KB

04.md

File metadata and controls

1255 lines (884 loc) · 51.1 KB

四、搜索和读取本地文件

在本章中,我们将介绍以下配方:

  • 爬网和搜索目录
  • 读取文本文件
  • 处理编码
  • 读取 CSV 文件
  • 读取日志文件
  • 读取文件元数据
  • 阅读图像
  • 阅读 PDF 文件
  • 阅读 Word 文档
  • 扫描文档以查找关键字

介绍

在本章中,我们将处理读取文件的基本操作,从搜索和打开目录和子目录中的文件开始。然后,我们将描述一些最常见的文件类型以及如何读取它们,包括原始文本文件、PDF 和 Word 文档等格式。

最后一个配方将把它们结合起来,展示如何在目录中递归搜索不同类型文件中的单词。

爬网和搜索目录

在本食谱中,我们将学习如何递归扫描目录以获取其中包含的所有文件。这些文件可以是特定类型的,也可以是全部文件。

准备

让我们首先创建一个包含一些文件信息的测试目录:

$ mkdir dir
$ touch dir/file1.txt
$ touch dir/file2.txt
$ mkdir dir/subdir
$ touch dir/subdir/file3.txt
$ touch dir/subdir/file4.txt
$ touch dir/subdir/file5.pdf
$ touch dir/file6.pdf

所有文件都将为空;我们将在本食谱中使用它们,只是为了发现它们。请注意,有四个文件具有.txt扩展名,两个文件具有.pdf扩展名。

The files are also available in the GitHub repository here: https://github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter04/documents/dir.

进入创建的dir目录:

$ cd dir

怎么做。。。

  1. 打印dir目录和子目录中的所有文件名:
>>> import os
>>> for root, dirs, files in os.walk('.'):
...     for file in files:
...         print(file)
...
file1.txt
file2.txt
file6.pdf
file3.txt
file4.txt
file5.pdf
  1. 打印文件的完整路径,与root连接:
>>> for root, dirs, files in os.walk('.'):
...     for file in files:
...         full_file_path = os.path.join(root, file)
...         print(full_file_path)
...
./dir/file1.txt
./dir/file2.txt
./dir/file6.pdf
./dir/subdir/file3.txt
./dir/subdir/file4.txt
./dir/subdir/file5.pdf
  1. 仅打印.pdf文件:
>>> for root, dirs, files in os.walk('.'):
...     for file in files:
...         if file.endswith('.pdf'):
...             full_file_path = os.path.join(root, file)
...             print(full_file_path)
...
./dir/file6.pdf
./dir/subdir/file5.pdf
  1. 仅打印包含偶数的文件:
>>> import re
>>> for root, dirs, files in os.walk('.'):
...     for file in files:
...         if re.search(r'[13579]', file):
...             full_file_path = os.path.join(root, file)
...             print(full_file_path)
...
./dir/file1.txt
./dir/subdir/file3.txt
./dir/subdir/file5.pdf

它是如何工作的。。。

os.walk()遍历整个目录和所有子目录,返回所有文件。它返回一个元组,其中包含特定目录、直接依赖的子目录以及所有文件:

>>> for root, dirs, files in os.walk('.'):
... print(root, dirs, files)
...
. ['dir'] []
./dir ['subdir'] ['file1.txt', 'file2.txt', 'file6.pdf']
./dir/subdir [] ['file3.txt', 'file4.txt', 'file5.pdf']

os.path.join()函数允许我们干净地连接两个路径,例如基本路径和文件。

由于文件以纯字符串的形式返回,因此可以进行任何类型的过滤,如步骤 3 所示。在步骤 4 中,可以使用正则表达式的全部功能进行过滤

在下一个配方中,我们将处理文件的内容,而不仅仅是文件名。

还有更多。。。

返回的文件不会以任何方式打开或修改。此操作是只读的。可以像往常一样打开文件,并按照以下方法进行描述。

Be aware that changing the structure of the directory while walking it may affect the results. If you need to store any file while working, for example, when copying or moving a file, it's usually a good idea to store it in a different directory.

os.path模块还有其他有趣的功能。除了join()之外,最有用的可能是:

  • os.path.abspath(),返回文件的绝对路径
  • os.path.split(),用于拆分目录和文件之间的路径:
>>> os.path.split('/a/very/long/path/file.txt')
('/a/very/long/path', 'file.txt')
  • os.path.exists(),返回文件系统上是否存在文件

有关os.path的完整文档可在此处找到:https://docs.python.org/3/library/os.path.html 。另一个模块pathlib可以以面向对象的方式用于更高级别的访问:https://docs.python.org/3/library/pathlib.html 。

如步骤 4 所示,可以使用多种过滤方式。第 1 章中显示的所有字符串操作,让我们开始我们的自动化旅程都可以使用。

另见

  • 第 1 章中引入正则表达式的配方,让我们开始我们的自动化之旅
  • 读取文本文件配方

读取文本文件

搜索特定文件后,我们可能会打开并读取它。文本文件是非常简单但功能非常强大的文件。它们以纯文本形式存储数据,没有复杂的二进制格式

文本文件支持是在 Python 中本地提供的,并且很容易将其视为行的集合。

准备

我们将阅读zen_of_python.txt文件,其中包含 Tim Peters 的Python 禅宗,这是一组格言,非常好地描述了 Python 背后的设计原则。可以在 GitHub 存储库中找到它:https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/documents/zen_of_python.txt :

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Python 的禅在这里的 PEP-20 中有描述:https://www.python.org/dev/peps/pep-0020/ 。

The Zen of Python can be displayed in any Python interpreter by calling import this.

怎么做。。。

  1. 逐行打开并打印整个文件(不显示结果):
>>> with open('zen_of_python.txt') as file:
...     for line in file:
...         print(line)
...
[RESULT NOT DISPLAYED]
  1. 打开文件并打印包含字符串should的任何行:
>>> with open('zen_of_python.txt', 'r') as file:
...     for line in file:
...         if 'should' in line.lower():
...             print(line)
...
Errors should never pass silently.
There should be one-- and preferably only one --obvious way to do it.
  1. 打开文件并打印包含单词better的第一行:
>>> with open('zen_of_python.txt', 'rt') as file:
...     for line in file:
...         if 'better' in line.lower():
...             print(line)
...             break
...
Beautiful is better than ugly.

它是如何工作的。。。

要以文本模式打开文件,请使用open()功能。这将返回一个file对象,然后可以对该对象进行迭代以逐行返回,如*如何执行…*部分的步骤 1 所示。

with上下文管理器是处理文件的一种非常方便的方法,因为它将在完成使用(离开块)后关闭文件。即使出现异常,它也会这样做。

步骤 2 展示了如何根据哪些行适用于我们的任务来迭代和过滤这些行。这些行作为字符串返回,可以通过多种方式进行过滤,如前所述。

可能不需要读取整个文件,如步骤 3 所示。因为逐行遍历文件将在运行时读取文件,所以可以随时停止,避免读取文件的其余部分。对于我们的示例这样的小文件,这不是很重要,但是对于长文件,这可以减少内存使用和时间。

还有更多。。。

with上下文管理器是处理文件的首选方法,但它不是唯一的方法。您也可以手动打开和关闭它们,如下所示:

>>> file = open('zen_of_python')
>>> content = file.read()
>>> file.close()

注意.close() 方法,以确保文件关闭并释放与打开文件相关的资源。.read()方法一次读取整个文件,而不是逐行读取。

The .read() method also accepts a size parameter in bytes that limits the size of the data read. For example, file.read(1024) will return up to 1 KB of information. The next call to .read() will continue from that point.

文件以特定模式打开。模式定义读/写以及文本或二进制数据的组合。默认情况下,文件以只读和文本模式打开,描述为'r'(步骤 2)或'rt'(步骤 3)。

更多模式将在其他配方中探索。

另见

  • 抓取和搜索目录配方
  • 处理编码配方

处理编码

文本文件可以采用不同的编码。近年来,这种情况有了很大的改善,但在使用不同的系统时仍然存在兼容性问题。

There's a difference between raw data in a file and a string object in Python. The string object has been transformed from whatever encoding the file contains into a native string. Once it is in this format, it may need to be stored in different encodings. By default, Python works with the defined by the OS, which in modern operating systems is UTF-8.  This is a highly compatible encoding, but you may need to save files in a different one.

准备

我们在 GitHub 存储库中准备了两个文件,用两种不同的编码存储字符串20£。一种是通常的 UTF8,另一种是 ISO 8859-1,另一种是通用编码。这些文件在 GitHub 的Chapter04/documents目录下可用,名称为example_iso.txt和example_utf8.txt:

https://github.com/PacktPublishing/Python-Automation-Cookbook

我们将使用美丽的汤模块,在第 3 章中的解析 HTML配方中介绍,构建您的第一个网页抓取应用。

怎么做。。。

  1. 打开example_utf8.txt文件并显示其内容:
>>> with open('example_utf8.txt') as file:
...     print(file.read())
...
20£
  1. 尝试打开example_iso.txt文件,将引发异常:
>>> with open('example_iso.txt') as file:
... print(file.read())
...
Traceback (most recent call last):
  ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa3 in position 2: invalid start byte
  1. 打开编码正确的example_iso.txt文件:
>>> with open('example_iso.txt', 
              encoding='iso-8859-1') as file:
...     print(file.read())
...
20£
  1. 打开utf8文件并将其内容保存在iso-8859-1文件中:
>>> with open('example_utf8.txt') as file:
...     content = file.read()
>>> with open('example_output_iso.txt', 'w',
               encoding='iso-8859-1') as file:
...     file.write(content)
...
4
  1. 最后,以正确的格式读取新文件,以确保正确保存:
>>> with open('example_output_iso.txt', 
              encoding='iso-8859-1') as file:
...     print(file.read())
...
20£

它是如何工作的。。。

*如何做……*部分中的步骤 1 和 2 非常简单。在步骤 3 中,我们添加了一个额外的参数encoding,以指定文件需要以不同于 UTF-8 的方式打开。

Python accepts a lot of standard encodings right out of the box. Check here for all of them and their aliases: https://docs.python.org/3/library/codecs.html#standard-encodings.

在步骤 4 中,我们在 ISO-8859-1 中创建一个新文件,并像往常一样写入该文件。注意'w'参数,它指定在文本模式下打开它进行写入

步骤 5 是确认文件已正确保存。

还有更多。。。

此配方假定我们知道文件的编码方式。但有时我们对此并不确定。Beauty Soup 是一个解析 HTML 的模块,可以尝试检测特定文件的编码方式。

Automatically detecting what encoding a file has may be, well, impossible, as there are potentially an infinte number of encodings. But we'll check the usual encodings that should cover 90% of the real world cases. Just remember that the easiest way of knowing for sure is to ask whomever created the file in the first place.

为此,我们需要使用'rb'参数打开文件以二进制格式读取,然后将二进制内容传递给 Beautiful Soup 的UnicodeDammit模块,如下所示:

>>> from bs4 import UnicodeDammit
>>> with open('example_output_iso.txt', 'rb') as file:
...     content = file.read()
...
>>> suggestion = UnicodeDammit(content)
>>> suggestion.original_encoding
'iso-8859-1'
>>> suggestion.unicode_markup
'20£\n'

然后可以推断编码。尽管.unicode_markup返回解码的字符串,但最好只使用此建议一次,然后在我们的自动任务中以正确的编码打开文件。

另见

  • 第一章中的操作字符串配方让我们开始我们的自动化之旅
  • 第 3 章中的解析 HTML配方构建您的第一个网页抓取应用

读取 CSV 文件

某些文本文件包含以逗号分隔的表格数据。这是一种创建结构化数据的方便方法,而不是使用专有的、更复杂的格式,如 Excel 或其他格式。这些文件称为逗号分隔值或CSV,文件和大多数电子表格包也会导出到它。

准备

我们已经准备了一个 CSV 文件,该文件使用了影院上座率排名前十的电影的数据,如本页所述:http://www.mrob.com/pub/film-video/topadj.html 。

我们将表格的前十个元素复制到电子表格程序(数字)中,并将文件导出为 CSV。该文件可在 GitHub 存储库中的Chapter04/documents目录中以top_films.csv的形式获得:

怎么做。。。

  1. 导入csv模块:
>>> import csv
  1. 打开该文件,创建一个读卡器,并遍历它以显示所有行的表格数据(仅显示三行):
>>> with open('top_films.csv') as file:
...   data = csv.reader(file)
...   for row in data:
...       print(row)
...
['Rank', 'Admissions\n(millions)', 'Title (year) (studio)', 'Director(s)']
['1', '225.7', 'Gone With the Wind (1939)\xa0(MGM)', 'Victor Fleming, George Cukor, Sam Wood']
['2', '194.4', 'Star Wars (Ep. IV: A New Hope) (1977)\xa0(Fox)', 'George Lucas']
...
['10', '118.9', 'The Lion King (1994)\xa0(BV)', 'Roger Allers, Rob Minkoff']
  1. 打开文件,使用DictReader构造数据,包括表头:
>>> with open('top_films.csv') as file:
...     data = csv.DictReader(file)
...     structured_data = [row for row in data]
...
>>> structured_data[0]
OrderedDict([('Rank', '1'), ('Admissions\n(millions)', '225.7'), ('Title (year) (studio)', 'Gone With the Wind (1939)\xa0(MGM)'), ('Director(s)', 'Victor Fleming, George Cukor, Sam Wood')])
  1. structured_data中的每一项都是一个完整的字典,包含以下每一个值:
>>> structured_data[0].keys()
odict_keys(['Rank', 'Admissions\n(millions)', 'Title (year) (studio)', 'Director(s)'])
>>> structured_data[0]['Rank']
'1'
>>> structured_data[0]['Director(s)']
'Victor Fleming, George Cukor, Sam Wood'

它是如何工作的。。。

请注意,需要读取该文件,我们使用with上下文管理器。这样可以确保文件在块的末尾关闭。

如*How to it…*部分的步骤 2 所示,csv.reader类允许我们按照表数据的格式,通过将代码返回行细分为列表来构造代码返回行。请注意,所有值都是如何描述为字符串的。csv.reader不知道第一行是否为表头。

对于更结构化的文件读取,在步骤 3 中,我们使用csv.DictReader,默认情况下,它读取第一行作为定义后面描述的字段的标题,然后将每一行转换为包含这些字段的字典。

Sometimes, like in this case, the names of the fields as described in the file can be a little verbose. Don't be afraid to translate the dictionary on an extra step into more manageable field names.

还有更多。。。

由于 CSV 是一种定义非常松散的解释,有几种方法可以存储数据。这在csv模块中表示为方言。例如,值可以用逗号、分号或制表符分隔。通过调用csv.list_dialect可以显示默认接受的方言列表。

By default, the dialect will be Excel, which is the most common one. Even other spreadsheets will commonly use it.

但是方言也可以通过Sniffer类从文件本身推断出来。Sniffer类分析文件(或整个文件)的一个样本,并返回一个dialect对象,以允许以正确的方式读取。

请注意,打开该文件时没有新行,因此不对其进行任何假设:

>>> with open('top_films.csv', newline='') as file:
...    dialect = csv.Sniffer().sniff(file.read())

打开读卡器时可以使用方言。再次注意newline,因为方言会正确地分割线条:

>>> with open('top_films.csv', newline='') as file:
...     reader = csv.reader(file, dialect)
...     for row in reader:
...         print(row)

完整的csv模块文档可在此处找到:https://docs.python.org/3.6/library/csv.html 。

另见

  • 处理编码配方
  • 读取文本文件配方

读取日志文件

另一种常见的结构化文本文件格式是日志文件。日志文件由日志行组成,日志行是具有特定格式的文本行。通常,每个事件都有发生的时间,因此该文件是事件的有序集合。

准备

包含五个销售日志的example_log.log文件可以从 GitHub 存储库中获取:https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/documents/example_logs.log 。

格式如下:

[<Timestamp in iso format>] - SALE - PRODUCT: <product id> - PRICE: $<price of the sale>

我们将使用Chapter01/price_log.py文件将每个日志处理为一个对象。

怎么做。。。

  1. 进口PriceLog:
>>> from price_log import PriceLog
  1. 打开日志文件并分析所有日志:
>>> with open('example_logs.log') as file:
...     logs = [PriceLog.parse(log) for log in file]
...
>>> len(logs)
5
>>> logs[0]
<PriceLog (Delorean(datetime=datetime.datetime(2018, 6, 17, 22, 11, 50, 268396), timezone='UTC'), 1489, 9.99)>
  1. 按所有销售确定总收入:
>>> total = sum(log.price for log in logs)
>>> total
Decimal('47.82')
  1. 确定每个product_id已售出多少套:
>>> from collections import Counter
>>> counter = Counter(log.product_id for log in logs)
>>> counter
Counter({1489: 2, 4508: 1, 8597: 1, 3086: 1})
  1. 过滤日志以查找所有出现的销售产品 ID1489:
>>> logs = []
>>> with open('example_logs.log') as file:
...     for log in file:
...         plog = PriceLog.parse(log)
...         if plog.product_id == 1489:
...             logs.append(plog)
...
>> len(logs)
2
>>> logs[0].product_id, logs[0].timestamp
(1489, Delorean(datetime=datetime.datetime(2018, 6, 17, 22, 11, 50, 268396), timezone='UTC'))
>>> logs[1].product_id, logs[1].timestamp
(1489, Delorean(datetime=datetime.datetime(2018, 6, 17, 22, 11, 50, 268468), timezone='UTC'))

它是如何工作的。。。

由于每个日志都是一行,因此我们打开该文件并逐个进行解析。解析代码可在price_log.py上找到。查看更多详细信息。

在*如何做…*部分的步骤 2 中,我们打开文件并处理每一行,创建一个包含所有已处理日志的日志列表。然后,我们可以生成聚合操作,如下面的步骤所示。

步骤 3 显示了如何聚合所有值,在本例中,将通过日志文件销售的所有项目的价格相加,以获得总收入。

步骤 4 使用计数器确定文件日志中每个项目的数量。这将返回一个类似字典的对象,其中包含要计数的值以及它们出现的次数。

过滤也可以逐行进行,如步骤 5 所示。这与本章配方中的其他过滤类似。

还有更多。。。

请记住,您可以在获得所需的所有数据后立即停止处理文件。如果文件非常大,这可能是一个很好的策略,日志文件通常就是这样。

计数器是一个很好的工具,可以快速计算列表。有关更多详细信息,请参见此处的 Python 文档:https://docs.python.org/2/library/collections.html#counter-对象。您可以通过调用以下命令获取订购的项目:

>>> counter.most_common()
[(1489, 2), (4508, 1), (8597, 1), (3086, 1)]

另见

  • 使用第三方工具解析第一章中的配方让我们开始我们的自动化之旅
  • 读取文本文件配方

读取文件元数据

文件元数据是与特定文件相关联的所有内容,而不是数据本身。这意味着参数,如文件大小、创建日期或其权限。

浏览这些数据很重要,例如,要过滤比日期早的文件,或者查找所有大于 KBs 值的文件。在这个配方中,我们将看到如何在 Python 中访问文件元数据。

准备

我们将使用 GitHub 存储库(中提供的zen_of_python.txt文件 https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/documents/zen_of_python.txt )。通过使用ls命令可以看到,该文件有856字节,在本例中,它是在 6 月 14 日创建的:

$ ls -lrt zen_of_python.txt
-rw-r--r--@ 1 jaime staff 856 14 Jun 21:22 zen_of_python.txt

在您的计算机上,日期可能因您下载代码的时间而异。

怎么做。。。

  1. 进口os和datetime:
>>> import os
>>> from datetime import datetime
  1. 检索zen_of_python.txt文件的统计信息:
>>> stats = os.stat(('zen_of_python.txt')
>>> stats
os.stat_result(st_mode=33188, st_ino=15822537, st_dev=16777224, st_nlink=1, st_uid=501, st_gid=20, st_size=856, st_atime=1529461935, st_mtime=1529007749, st_ctime=1529007757)
  1. 获取文件的大小(以字节为单位):
>>> stats.st_size
856
  1. 获取上次修改文件的时间:
>>> datetime.fromtimestamp(stats.st_mtime)
datetime.datetime(2018, 6, 14, 21, 22, 29)
  1. 获取上次访问文件的时间:
>>> datetime.fromtimestamp(stats.st_atime)
datetime.datetime(2018, 6, 20, 3, 32, 15)

它是如何工作的。。。

os.stats返回一个 stats 对象,该对象表示存储在文件系统中的元数据。元数据包括:

  • 文件的大小,以字节为单位,如*如何操作…*部分中的步骤 3 所示,使用st_size
  • 上次修改文件内容时,如步骤 4 所示,使用st_mtime
  • 上次读取(访问)文件时,如步骤 5 所示,使用st_atime

时间作为时间戳返回,因此在步骤 4 和 5 中,我们根据时间戳创建一个datetime对象,以便更好地访问数据。

所有这些值都可用于筛选文件。

Notice you don't need to open the file with open() to read its metadata. Detecting whether a file has been changed after a known value will be quicker than comparing its content, so you can take advantage of that for comparison.

还有更多。。。

为了逐个获取统计信息,os.path中还提供了方便功能,遵循get<value>模式:

>>> os.path.getsize('zen_of_python.txt')
856
>>> os.path.getmtime('zen_of_python.txt')
1529531584.0
>>> os.path.getatime('zen_of_python.txt')
1529531669.0

该值以 UNIX 时间戳格式指定(自 1970 年 1 月 1 日起的秒数)。

Notice calling these three functions will be slower than calling os.stats and processing the results. Also, returned stats can be inspected to detect the available values.

配方中描述的值适用于所有文件系统,但还可以使用更多的值。

例如,要获取文件的创建日期,可以使用 MacOS 的st_birthtime参数或 Windows 中的st_mtime参数。

st_mtime is always available, but its meaning changes between systems. In Unix systems, it will change when the content is modified, so it's not a reliable time of creation.

os.stat将遵循符号链接。如果要获取符号链接的统计信息,请使用os.lstat()。

检查此处所有可用统计数据的完整文档:https://docs.python.org/3.6/library/os.html#os.stat_result 。

另见

  • 读取文本文件配方
  • 读图配方

阅读图像

最常见的非文本数据可能是图像数据。图像有自己的一组特定元数据,可以读取这些元数据以过滤值或执行其他操作。

一个主要挑战是处理多种格式和不同的元数据定义。我们将在此配方中展示如何从 JPEG 和 PNG 中获取信息,以及如何对相同的信息进行不同的编码。

准备

可以说,在 Python 中处理图像的最佳通用工具包是枕头。此模块允许您轻松读取最常见格式的文件,并对其执行操作。枕头最初是PIL(Python 图像库)的一个分支,这是几年前停滞不前的一个模块。

我们还将使用xmltodict模块将一些 XML 数据转换为更方便的字典。将两个模块添加到requirements.txt并重新安装到虚拟环境中:

$ echo "Pillow==5.1.0" >> requirements.txt
$ echo "xmltodict==0.11.0" >> requirements.txt
$ pip install -r requirements.txt

照片文件中的元数据信息在EXIF(可交换图像文件格式)中定义。EXIF 是存储照片信息的标准,包括相机拍摄的内容、拍摄时间、GPS 位置、曝光、焦距、颜色信息等。

You can get a good summary here: https://www.slrphotographyguide.com/what-is-exif-metadata/. All the information is optional, but virtually all digital cameras and processing software will store some data. Because of the privacy concerns, parts of it, like the exact location, can be disabled.

以下图片将用于此配方,可在 GiHub 存储库(中下载 https://github.com/PacktPublishing/Python-Automation-Cookbook/tree/master/Chapter04/images :

  • photo-dublin-a1.jpg
  • photo-dublin-a2.png
  • photo-dublin-b.png

其中两张,photo-dublin-a1.jpg和photo-dublin-a2.png是同一张照片,但第一张是原始照片,第二张经过修饰,略微改变颜色并进行裁剪。请注意,一个是 JPEG 格式,另一个是 PNG 格式。另一幅,photo-dublin-b.png 是另一幅图片。这两张照片都是在都柏林用同一台手机摄像头在两个不同的日子拍摄的。

虽然 Pillow 了解 JPG 文件如何直接存储 EXIF 信息,但 PNG 文件存储 XMP 信息,这是一种更通用的标准,可以包含 EXIF 信息

More info about XMP can be obtained here: https://www.adobe.com/devnet/xmp.html. For the most part, it defines an XML tree structure that's relatively readable in raw.

更复杂的是,XMP 是 RDF 的子集,RDF 是描述信息编码方式的标准。

If EFIX, XMP, and RDF sounds confusing, well, it's because they are. Ultimately, they are just names to store the values we are interested in. We can inspect the specifics of the names using Python introspection tools and check exactly how the data is structured and what the name of the parameter we are looking for is.

由于 GPS 信息以不同的格式存储,我们在 GitHub 存储库中包含了一个名为gps_conversion.py的文件,这里是:https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapter04/gps_conversion.py 。这包括功能exif_to_decimal和rdf_to_decimal,这将把两种格式转换成小数来比较它们。

怎么做。。。

  1. 导入要在此配方中使用的模块和函数:
>>> from PIL import Image
>>> from PIL.ExifTags import TAGS, GPSTAGS
>>> import xmltodict
>>> from gps_conversion import exif_to_decimal, rdf_to_decimal
  1. 打开第一张照片:
>>> image1 = Image.open('photo-dublin-a1.jpg')
  1. 获取文件的宽度、高度和格式:
>>> image1.height
3024
>>> image1.width
4032
>>> image1.format
'JPEG'
  1. 检索图像的 EXIF 信息,并将其处理为方便的字典。显示相机、使用的镜头以及拍摄时间:
>>> exif_info_1 = {TAGS.get(tag, tag): value 
                   for tag, value in image1._getexif().items()}
>>> exif_info_1['Model']
'iPhone X'
>>> exif_info_1['LensModel']
'iPhone X back dual camera 4mm f/1.8'
>>> exif_info_1['DateTimeOriginal']
'2018:04:21 12:07:55'
  1. 打开第二个图像并获取 XMP 信息:
>>> image2 = Image.open('photo-dublin-a2.png')
>>> image2.height
2630
>>> image2.width
3943
>>> image2.format
'PNG'
>>> xmp_info = xmltodict.parse(image2.info['XML:com.adobe.xmp'])
  1. 获取 RDF 描述字段,其中包含我们要查找的所有值。检索模型(TIFF 值)、镜头模型(EXIF 值)和创建日期(XMP 值)。检查值是否与步骤 4 中的值相同,即使文件不同:
>>> rdf_info_2 = xmp_info['x:xmpmeta']['rdf:RDF']['rdf:Description']
>>> rdf_info_2['tiff:Model']
'iPhone X'
>>> rdf_info_2['exifEX:LensModel']
'iPhone X back dual camera 4mm f/1.8'
>>> rdf_info_2['xmp:CreateDate']
'2018-04-21T12:07:55'
  1. 获取两张图片中的 GPS 信息,转换为等效格式,并检查它们是否相同。请注意,分辨率不同,但它们匹配到第四个小数点:
>>> gps_info_1 = {GPSTAGS.get(tag, tag): value 
                  for tag, value in exif_info_1['GPSInfo'].items()}
>>> exif_to_decimal(gps_info_1)
('N53.34690555555556', 'W6.247797222222222')
>>> rdf_to_decimal(rdf_info_2)
('N53.346905', 'W6.247796666666667')
  1. 打开第三张图片,获取创建日期和 GPS 信息,并检查它是否与另一张图片不匹配,尽管它很接近(第二个和第三个小数点不相同):
>>> image3 = Image.open('photo-dublin-b.png')
>>> xmp_info = xmltodict.parse(image3.info['XML:com.adobe.xmp'])
>>> rdf_info_3 = xmp_info['x:xmpmeta']['rdf:RDF']['rdf:Description']
>>> rdf_info_3['xmp:CreateDate']
'2018-03-08T18:16:57'
>>> rdf_to_decimal(rdf_info_3)
('N53.34984166666667', 'W6.260388333333333')

它是如何工作的。。。

Pillow 能够以最常见的语言解释文件,并以 JPG 格式将其作为图像打开,如*如何操作…*部分中的步骤 2 所示。

Image对象包含文件大小和格式的基本信息,在步骤 3 中显示。info属性包含依赖于格式的信息。

JPG 文件的 EXIF 元数据可以使用._getexif()方法进行解析,但随后需要正确翻译,因为它使用原始二进制定义。例如,数字 42036 对应于LensModel属性。幸运的是,PIL.ExifTags模块中有一个所有标记的定义。在步骤 4 中,我们将字典翻译成可读标记,以获得更可读的字典。

步骤 5 打开一个 PNG 格式,该格式具有与大小相关的相同属性,但元数据以 XML/RDF 格式存储,需要借助xmltodict.进行解析。步骤 6 显示了如何导航此元数据以提取与 JPG 格式相同的信息。数据是相同的,因为两个文件来自相同的原始图片,即使图像不同。

xmltodict has some issues when trying to parse data that's not in XML format. Check that the input is valid XML.

步骤 7 提取了以不同方式存储的两幅图像的 GPS 信息,并显示它们是相同的(尽管由于编码方式的不同,精度不同)。

步骤 8 显示不同照片上的信息。

还有更多。。。

枕头还有很多修改图片的功能。调整文件大小或对其进行简单修改(如旋转)非常容易。您可以在此处找到完整的枕头文档:https://pillow.readthedocs.io 。

Pillow allow a lot of operations with images. Not only simple operations such as resizing or transforming one format into another, but also things like cropping the image, applying color filters, or generating animated GIFs. If you're interested in image processing using Python, it is definitely something to take a look at.

配方中的 GPS 坐标以DMS(度、分、秒、DDM(度、十进制分)表示,并转换为DD(十进制度)。您可以在此处找到有关不同 GPS 格式的更多信息:http://www.ubergizmo.com/how-to/read-gps-coordinates/ 。如果你好奇的话,你还会发现如何在那里搜索图片的确切位置。

读取图像文件的更高级用途是尝试将其处理为OCR(光学字符识别)。这意味着自动检测图像中的文本并对其进行提取和处理。开源模块tesseract允许您这样做,它可以与 Python 和 Pillow 一起使用。

您需要在您的系统中安装tesseract(https://github.com/tesseract-ocr/tesseract/wiki )和pytesseractPython 模块(使用pip install pytesseract。您可以从位于的 GitHub 存储库下载一个名为photo-text.jpg的明文文件 https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapterimg/photo-text.jpg

>>> from PIL import Image
>>> import pytesseract
>>> pytesseract.image_to_string(Image.open('photo-text.jpg'))
'Automate!'

如果图像中的文本不是很清晰,或者与图像混合,或者使用独特的字体,OCR 可能会很困难。在photo-dublin-a-text.jpg文件中有一个例子,(可在的 GitHub 存储库中找到)https://github.com/PacktPublishing/Python-Automation-Cookbook/blob/master/Chapterimg/photo-dublin-a-text.jpg ),包括图片上方的文字:

>>> >>> pytesseract.image_to_string(Image.open('photo-dublin-a-text.jpg'))
'fl\n\nAutomat'

有关 Tesseract 的更多信息,请访问以下链接: https://github.com/tesseract-ocr/tesseract https://github.com/madmaze/pytesseract

Properly importing files to OCR may require initial image processing for better results. Image processing is out of scope for the objectives of this book, but you may use OpenCV, which more powerful than Pillow. You can process a file and then open it with Pillow: http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_tutorials.html.

另见

  • 读取文本文件配方
  • 读取文件元数据配方
  • 抓取和搜索目录配方

阅读 PDF 文件

文档的常用格式是PDF(便携文档格式)。它最初是一种格式,用于描述任何打印机的文档,因此 PDF 是一种格式,可确保文档按其显示的内容准确打印,因此是保证一致性的一种好方法。它已成为共享文档,特别是只读文档的强大标准。

准备

对于这个配方,我们将使用PyPDF2模块。我们需要将其添加到虚拟环境中:

>>> echo "PyPDF2==1.26.0" >> requirements.txt
>>> pip install -r requirements.txt

在 GitHub 目录Chapter03/documents中,我们准备了两份文件document-1.pdf和document-2.pdf用于本配方。请注意,它们主要包含 Lorem Ipsum 文本,这只是占位符文本。

Lorem Ipsum text is commonly used in design to show text without needing to create the content before the design. Learn more about it here: https://loremipsum.io/.

它们都是相同的测试文档,但第二个文档只能使用密码打开。密码为automate。

怎么做。。。

  1. 导入模块:
>>> from PyPDF2 import PdfFileReader
  1. 打开document-1.pdf文件并创建 PDF 文档对象。请注意,整个读取过程中需要打开文件:
>>> file = open('document-1.pdf', 'rb')
>>> document = PdfFileReader(file)
  1. 获取文档的页数,并检查文档是否未加密:
>>> document.numPages
3
>>> document.isEncrypted
False
  1. 从文档信息(2018-Jun-24 11:15:18中获取创建日期,发现它是用 MacQuartz PDFContext创建的:
>>> document.documentInfo['/CreationDate']
"D:20180624111518Z00'00'"
>>> document.documentInfo['/Producer']
'Mac OS X 10.13.5 Quartz PDFContext'
  1. 获取第一页,并阅读上面的文本:
>>> document.pages[0].extractText()
'!A VERY IMPORTANT DOCUMENT \nBy James McCormac CEO Loose Seal Inc '
  1. 对第二页执行相同的操作(此处编辑):
>>> document.pages[1].extractText()
'"!This is an example of a test document that is stored in PDF format. It contains some \nsentences to describe what it is and the it has lore ipsum text.\n!"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. ...$'
  1. 关闭文件并打开document-2.pdf:
>>> file.close()
>>> file = open('document-2.pdf', 'rb')
>>> document = PdfFileReader(file)
  1. 检查文档是否加密(需要密码),如果尝试访问其内容,则会引发错误:
>>> document.isEncrypted
True
>>> document.numPages
...
PyPDF2.utils.PdfReadError: File has not been decrypted
  1. 解密文件并访问其内容:
>>> document.decrypt('automate')
1
>>> document.numPages
3
>>> document.pages[0].extractText()
'!A VERY IMPORTANT DOCUMENT \nBy James McCormac CEO Loose Seal Inc ' 
  1. 关闭要清理的文件:
>>> file.close()

它是如何工作的。。。

打开文档后,*如何操作…*部分中的步骤 1 和 2 显示,document对象提供对文档的访问。

最有趣的属性是.numPages中可用的页面数,以及.pages中可用的每个页面,这些页面可以像列表一样访问。

其他可访问的数据存储在.documentInfo中,它存储创建者及其创建时间的元数据。

The information in .documentInfo is optional and sometimes not up-to-date. It depends greatly on the tool used to generate the PDF.

每个page对象都可以通过调用.extractText()来获取其文本,这将返回页面中包含的所有文本,如步骤 5 和 6 所述。此方法尝试提取所有文本,但有一些限制。对于结构良好的文本,例如我们的示例,它工作得非常好,结果文本可以被干净地处理。处理多列文本或位于奇怪位置的文本可能会使处理复杂化。

Notice that the PDF file needs to be open for the whole operation, instead of using a with context operator. After leaving the with block, the file is closed.

步骤 8 和 9 显示了如何处理加密文件。您可以使用.isEncrypted检测文件是否加密,然后使用.decrypt方法解密,并给出密码。

还有更多。。。

PDF 格式非常灵活,非常标准,但这也意味着它可能很难解析和处理。

虽然大多数 PDF 文件包含文本信息,但包含图像的情况并不少见。例如,这种情况经常发生在扫描的文档中。这意味着信息存储为图像集合,而不是文本。这使得提取数据变得困难;我们最终不得不使用 OCR 等方法将图像解析为文本。

PyPDF2 没有提供处理图像的良好接口。您可能需要将 PDF 转换为一组图像,然后进行处理。大多数 PDF 阅读器都可以这样做,或者您可以使用命令行工具,如pdftooppm()https://linux.die.net/man/1/pdftoppm 或 QPDF(见下文)。有关 OCR 的想法,请参见阅读图像食谱。

PyPDF2 可能不理解某些加密文件的方法。将生成NotImplementedError: only algorithm code 1 and 2 are supported。如果发生这种情况,您需要从外部解密 PDF,并在解密后打开它。您可以使用 QPDF 创建不带密码的副本,如下所示:

$ qpdf --decrypt --password=PASSWORD encrypted.pdf output-decrypted.pdf

完整的 QPDF 可在此处获得:http://qpdf.sourceforge.net/files/qpdf-manual.html 。QPDF 在大多数包管理器中也可用。

QPDF is capable of doing a lot of transformations and analyzing PDFs in-depth. There are also bindings into Python on a module called pikepdf (https://pikepdf.readthedocs.io/en/stable/). This module is more difficult to use than PyPDF2 and it's not as straightforward for text extraction, but it can be useful if other operations such as extracting images from a PDF are required.

另见

  • 读取文本文件配方
  • 抓取和搜索目录配方

阅读 Word 文档

Word 文档(.docx是另一种常见的存储文本的文档。它们通常由 Microsoft Office 生成,但其他工具也会生成兼容的文件。它们可能是共享需要编辑的文件的最常见格式,但在分发文档时也很常见。

我们将在本配方中了解如何从 Word 文档中提取文本信息。

准备

我们将使用python-docx模块读取和处理 Word 文档:

>>> echo "python-docx==0.8.6" >> requirements.txt
>>> pip install -r requirements.txt

我们已经准备了一个测试文件,可以在 GitHubChapter04/documents目录中找到,名为document-1.docx,我们将在这个配方中使用它。请注意,本文档遵循配方读取 PDF 文件配方测试文档中描述的相同 Lorem Ipsun 模式。

怎么做。。。

  1. 进口python-docx:
>> import docx
  1. 打开document-1.docx文件:
>>> doc = docx.Document('document-1.docx')
  1. 检查core_properties中存储的一些元数据属性:
>> doc.core_properties.title
'A very important document'
>>> doc.core_properties.keywords
'lorem ipsum'
>>> doc.core_properties.modified
datetime.datetime(2018, 6, 24, 15, 1, 7)
  1. 检查段落数:
>>> len(doc.paragraphs)
58
  1. 浏览段落以检测包含文本的段落。请注意,此处并非显示所有文本:
>>> for index, paragraph in enumerate(doc.paragraphs):
...     if paragraph.text:
...         print(index, paragraph.text)
...
30 A VERY IMPORTANT DOCUMENT
31 By James McCormac
32 CEO Loose Seal Inc
34
...
56 TITLE 2
57 ...
  1. 获取与第一页标题和副标题相对应的段落30和31的文本:
>>> doc.paragraphs[30].text
'A VERY IMPORTANT DOCUMENT'
>>> doc.paragraphs[31].text
'By James McCormac'
  1. 每个段落都有runs,这是具有不同属性的文本部分。检查第一个文本段落和run是否为粗体,第二个是否为斜体:
>>> doc.paragraphs[30].runs[0].italic
>>> doc.paragraphs[30].runs[0].bold
True
>>> doc.paragraphs[31].runs[0].bold
>>> doc.paragraphs[31].runs[0].italic
True
  1. 在本文档中,大多数段落只有一个run,但我们在48段中有一个不同运行的好例子。显示其文本和不同的样式。例如,Word是粗体字,ipsum是斜体字:
>>> [run.text for run in doc.paragraphs[48].runs]
['This is an example of a test document that is stored in ', 'Word', ' format', '. It contains some ', 'sentences', ' to describe what it is and it has ', 'lore', 'm', ' ipsum', ' text.']
>>> run1 = doc.paragraphs[48].runs[1]
>>> run1.text
'Word'
>>> run1.bold
True
>>> run2 = doc.paragraphs[48].runs[8]
>>> run2.text
' ipsum'
>>> run2.italic
True

它是如何工作的。。。

Word 文档最重要的特点是数据是以段落而不是页面的形式组织的。字体大小、行大小和其他注意事项可能会使页数发生变化。

大多数段落通常也是空的,或者只包含新行、制表符或其他空白字符。检查段落是否为空并跳过它是一个好主意。

在*如何操作…*部分,步骤 2 打开文件,步骤 3 显示如何访问核心属性。这些属性在 Word 中定义为文档元数据,例如作者或创建日期。

This information needs to be taken with a grain of salt, as a lot of tools that produce Word documents (but not Microsoft Office) won't necessarily fill it. Double-check before using that information.

可以迭代文档的段落,并以原始格式提取其文本,如步骤 6 所示。这是不包括样式信息的信息,通常是自动处理数据最有用的信息。

如果需要样式信息,可以使用管路,如步骤 7 和 8 所示。每个段落可以包含一个或多个梯段,这些梯段是共享相同样式的较小单元。例如,如果一个句子是Word1word2word3,则会有三个运行,一个是斜体文本(Word1),另一个是下划线(word2),另一个是粗体(word3)。更重要的是,可以使用只包含空格的常规文本进行中间运行,总共运行 5 次。

可以在粗体、斜体或下划线等属性上单独检测样式

The division in runs can quite complicated. Due to the way editors work it, is not uncommon to have half-words, a split word in two runs, sometimes with the same properties. Do not rely on the number of runs and analyse the content. In particular, double-check if trying to ensure if a part with a particular style is divided in two or more runs. A good example is the words lore m (it should be lorem) in Step 8.

请注意,由于 Word 文档是由如此多的源生成的,许多属性可能无法设置,将其留给工具来确定要使用的具体内容。例如,保留默认字体非常常见,这可能意味着字体信息为空。

还有更多。。。

可在字体属性下找到更多样式信息,如small_caps或大小:

>>> run2.font.cs_italic
True
>>> run2.font.size
152400
>>> run2.font.small_caps

通常只关注原始文本,而不关注样式信息才是正确的解析。但有时一段话中的粗体字会有特殊的意义。它可能是标题,也可能是您要查找的结果。因为它是突出显示的,所以很可能就是你要找的!在分析文档时请记住这一点。

您可以在此处找到整个python-docx文档:https://python-docx.readthedocs.io/en/latest/ 。

另见

  • 读取文本文件配方
  • 读取 PDF 文件配方

扫描文档以查找关键字

在这个配方中,我们将加入前面配方的所有课程,并在目录中搜索特定关键字的文件。这是本章其余部分的概述,包括一个搜索不同类型文件的脚本。

准备

确保在requirements.txt文件中包含以下所有模块,并将其安装到您的虚拟环境中:

beautifulsoup4==4.6.0
Pillow==5.1.0
PyPDF2==1.26.0
python-docx==0.8.6

检查要搜索的目录是否包含以下文件(所有文件都在 GitHub 中的Chapter04/documents目录中可用)。请注意,为简单起见,file5.pdf和file6.pdf是document-1.pdf的副本。file1.txt至file4.txt为空文件:

├── dir
│   ├── file1.txt
│   ├── file2.txt
│   ├── file6.pdf
│   └── subdir
│       ├── file3.txt
│       ├── file4.txt
│       └── file5.pdf
├── document-1.docx
├── document-1.pdf
├── document-2-1.pdf
├── document-2.pdf
├── example_iso.txt
├── example_output_iso.txt
├── example_utf8.txt
├── top_films.csv
└── zen_of_python.txt

我们准备了一个脚本scan.py,它将在所有.txt、.csv、.pdf和.docx文件中搜索一个单词。该脚本位于 GitHub 存储库的Chapter04目录中。

怎么做。。。

  1. 如何使用scan.py脚本,请参考帮助-h:
$ python scan.py -h
usage: scan.py [-h] [-w W]

optional arguments:
 -h, --help show this help message and exit
 -w W Word to search
  1. 搜索出现在大多数文件中的单词the:
$ python scan.py -w the
>>> Word found in ./document-1.pdf
>>> Word found in ./top_films.csv
>>> Word found in ./zen_of_python.txt
>>> Word found in ./dir/file6.pdf
>>> Word found in ./dir/subdir/file5.pdf
  1. 搜索单词lorem,仅出现在 PDF 和 docx 文件中:
$ python scan.py -w lorem
>>> Word found in ./document-1.docx
>>> Word found in ./document-1.pdf
>>> Word found in ./dir/file6.pdf
>>> Word found in ./dir/subdir/file5.pdf
  1. 搜索单词20£,仅出现在两个 ISO 文件中,编码不同:
$ python scan.py -w 20£
>>> Word found in ./example_iso.txt
>>> Word found in ./example_output_iso.txt
  1. 搜索不区分大小写。搜索单词BETTER,仅出现在zen_of_python.txt文件中:
$ python scan.py -w BETTER
>>> Word found in ./zen_of_python.txt

它是如何工作的。。。

文件scan.py包含以下元素:

  1. 解析输入参数并为命令行创建帮助的入口点。
  2. 一个主函数,它遍历目录并分析找到的每个文件。根据它们的扩展,它决定是否有可用的函数来处理和搜索它。
  3. 一个EXTENSION字典,它将扩展名与搜索它们的函数配对。
  4. search_txt、search_csv、search_pdf和search_docx功能处理和搜索每种文件所需的单词。

比较不区分大小写,因此搜索词以小写形式转换,并且在所有比较中,文本都转换为小写形式。

每个搜索功能都有自己的特点:

  1. search_txt首先使用UnicodeDammit打开文件以确定其编码,然后打开文件并逐行读取。如果找到该单词,它将立即停止并返回 success。
  2. search_csv以 CSV 格式打开文件,不仅逐行迭代,而且逐列迭代。一旦找到单词,它就会返回。
  3. search_pdf打开文件并在加密后退出。它不是,而是一页一页地提取文本并与单词进行比较。一旦找到匹配项,它就会返回。
  4. search_docx打开该文件并遍历其所有段落以进行匹配。一旦找到匹配项,函数就会返回。

还有更多。。。

有一些额外的想法可以实施:

  • 可以添加更多搜索功能。在本章中,我们浏览了日志文件和图像。
  • 类似的结构可以用于搜索文件并仅返回最后 10 个文件。
  • search_csv不是嗅探方言。这也可以加上。
  • 阅读是循序渐进的。应该可以并行读取文件,对其进行分析以获得更快的返回,但请注意,并行读取文件可能会导致排序问题,因为文件的处理顺序并不总是相同的。

另见

  • 抓取和搜索目录配方
  • 读取文本文件配方
  • 处理编码配方
  • 读取 CSV 文件配方
  • 读取 PDF 文件配方
  • 阅读 Word 文档配方