This is a [ Personal Note ]

单纯记录下这次写定向爬虫中遇到的问题。其实很多东西只是因为忘了,不是因为没学过,这次记录下来以便日后回顾。

rsync

rsync命令

rsync相比scp有很多优势,可以完全替代。

Demo

-P的作用等同于--partial--progress参数,前者可以让传输实现断点续传,后者可以显示进度条。

Manual

语法

1
2
3
4
5
6
rsync [OPTION]... SRC DEST
rsync [OPTION]... SRC [USER@]host:DEST
rsync [OPTION]... [USER@]HOST:SRC DEST
rsync [OPTION]... [USER@]HOST::SRC DEST
rsync [OPTION]... SRC [USER@]HOST::DEST
rsync [OPTION]... rsync://[USER@]HOST[:PORT]/SRC [DEST]

对应于以上六种命令格式,rsync有六种不同的工作模式:

  1. 拷贝本地文件。当SRC和DES路径信息都不包含有单个冒号":"分隔符时就启动这种工作模式。如:rsync -a /data /backup
  2. 使用一个远程shell程序(如rshssh)来实现将本地机器的内容拷贝到远程机器。当DST路径地址包含单个冒号":"分隔符时启动该模式。如:rsync -avz *.c foo:src
  3. 使用一个远程shell程序(如rsh、ssh)来实现将远程机器的内容拷贝到本地机器。当SRC地址路径包含单个冒号":"分隔符时启动该模式。如:rsync -avz foo:src/bar /data
  4. 从远程rsync服务器中拷贝文件到本地机。当SRC路径信息包含"::"分隔符时启动该模式。如:rsync -av [email protected]::www /databack
  5. 从本地机器拷贝文件到远程rsync服务器中。当DST路径信息包含"::"分隔符时启动该模式。如:rsync -av /databack [email protected]::www
  6. 列远程机的文件列表。这类似于rsync传输,不过只要在命令中省略掉本地机信息即可。如:rsync -v rsync://192.168.78.192/www

选项

1
2
3
4
5
6
7
8
9
-v, --verbose 详细模式输出。
-q, --quiet 精简输出模式。
-c, --checksum 打开校验开关,强制对文件传输进行校验。
-a, --archive 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD。
-r, --recursive 对子目录以递归模式处理。
-R, --relative 使用相对路径信息。
...
...
...

screen

screen命令

GNU’s Screen

nohup不香,screen更方便。

Demo

1
2
3
4
5
6
7
$ screen -S py_script

$ screen -ls



$ screen -r py_script

Manual

语法

1
screen [-AmRvx -ls -wipe][-d <作业名称>][-h <行数>][-r <作业名称>][-s ][-S <作业名称>]

选项

1
2
3
4
5
6
7
8
9
10
11
12
-A  将所有的视窗都调整为目前终端机的大小。
-d <作业名称>  将指定的screen作业离线。
-h <行数>  指定视窗的缓冲区行数。
-m  即使目前已在作业中的screen作业,仍强制建立新的screen作业。
-r <作业名称>  恢复离线的screen作业。
-R  先试图恢复离线的作业。若找不到离线的作业,即建立新的screen作业。
-s  指定建立新视窗时,所要执行的shell。
-S <作业名称>  指定screen作业的名称。
-v  显示版本信息。
-x  恢复之前离线的screen作业。
-ls或--list  显示目前所有的screen作业。
-wipe  检查目前所有的screen作业,并删除已经无法使用的screen作业。

常用screen参数

1
2
3
4
5
screen -S yourname -> 新建一个叫yourname的session
screen -ls -> 列出当前所有的session
screen -r yourname -> 回到yourname这个session
screen -d yourname -> 远程detach某个session
screen -d -r yourname -> 结束当前session并回到yourname这个session

Python3 utf-8

1

1
PYTHONIOENCODING=utf-8 python3 main.py
1
open('./file.txt', 'w', 'utf-8')
1
logging.FileHandler(encoding='utf-8')

以及所有有io的地方encoding='utf-8'

argparse

python3 cookbook

arhparse - Parser for command-line options, arguments and sub-commands

argparse包是处理命令行参数最好的工具,你的每个参数都可以调整众多选项

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
import argparse
parser = argparse.ArgumentParser(description='A script')
parser.add_argument('-v', dest='verbose',
action='store_true',
help='verbose mode')
parser.add_argument('-o', dest='outfile',
action='store',
help='output file')
args = parser.parse_args()

print(args.verbose)
print(args.outfile)

logging

Python日志库logging总结 by Wizey

Python logging同时输出到屏幕和文件

Logging facility for Python - Python3 documentation

不管是输出重定向还是tee命令,都不如专门的日志模块好用。

Demo

1
2
3
4
5
6
7
8
import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

logging同时输出到屏幕和文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
logger = logging.getLogger()  
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s %(name)s:%(levelname)s:%(message)s',
datefmt='%Y-%m-%d %H:%M:%S')


fh = logging.FileHandler(str(datetime.now().strftime('%Y-%m-%d_%H:%M:%S')) + '_ftban.log',
mode='a',
encoding='utf-8')
fh.setLevel(logging.INFO)
fh.setFormatter(formatter)


sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)


logger.addHandler(fh)
logger.addHandler(sh)

sqlite

sqlite3 - DB-API 2.0 interface for SQLitedatabases

SQLite - Python

python3操作sqlite3是非常方便的,定向爬虫用sqlite3储存数据基本是够用了。

Demo

1
2
3
4
with sqlite3.connect(db_path) as conn:
db_cursor = conn.cursor()
db_cursor.execute("DROP TABLE your-table-name;")
conn.commit()
1
2
3
4
5
with sqlite3.connect(db_path) as conn:
db_cursor = conn.cursor()
db_cursor.execute("SELECT * FROM mytable;")
print(db_cursor.fetchone())
print(db_cursor.fetchall())

APIs

No API & Description
1 sqlite3.connect(database [,timeout ,other optional arguments]) 该 API 打开一个到 SQLite 数据库文件 database 的链接。您可以使用 “:memory:” 来在 RAM 中打开一个到 database 的数据库连接,而不是在磁盘上打开。如果数据库成功打开,则返回一个连接对象。当一个数据库被多个连接访问,且其中一个修改了数据库,此时 SQLite 数据库被锁定,直到事务提交。timeout 参数表示连接等待锁定的持续时间,直到发生异常断开连接。timeout 参数默认是 5.0(5 秒)。如果给定的数据库名称 filename 不存在,则该调用将创建一个数据库。如果您不想在当前目录中创建数据库,那么您可以指定带有路径的文件名,这样您就能在任意地方创建数据库。
2 connection.cursor([cursorClass]) 该例程创建一个 cursor,将在 Python 数据库编程中用到。该方法接受一个单一的可选的参数 cursorClass。如果提供了该参数,则它必须是一个扩展自 sqlite3.Cursor 的自定义的 cursor 类。
3 cursor.execute(sql [, optional parameters]) 该例程执行一个 SQL 语句。该 SQL 语句可以被参数化(即使用占位符代替 SQL 文本)。sqlite3 模块支持两种类型的占位符:问号和命名占位符(命名样式)。例如:cursor.execute(“insert into people values (?, ?)”, (who, age))
4 connection.execute(sql [, optional parameters]) 该例程是上面执行的由光标(cursor)对象提供的方法的快捷方式,它通过调用光标(cursor)方法创建了一个中间的光标对象,然后通过给定的参数调用光标的 execute 方法。
5 cursor.executemany(sql, seq_of_parameters) 该例程对 seq_of_parameters 中的所有参数或映射执行一个 SQL 命令。
6 connection.executemany(sql[, parameters]) 该例程是一个由调用光标(cursor)方法创建的中间的光标对象的快捷方式,然后通过给定的参数调用光标的 executemany 方法。
7 cursor.executescript(sql_script) 该例程一旦接收到脚本,会执行多个 SQL 语句。它首先执行 COMMIT 语句,然后执行作为参数传入的 SQL 脚本。所有的 SQL 语句应该用分号(;)分隔。
8 connection.executescript(sql_script) 该例程是一个由调用光标(cursor)方法创建的中间的光标对象的快捷方式,然后通过给定的参数调用光标的 executescript 方法。
9 connection.total_changes() 该例程返回自数据库连接打开以来被修改、插入或删除的数据库总行数。
10 connection.commit() 该方法提交当前的事务。如果您未调用该方法,那么自您上一次调用 commit() 以来所做的任何动作对其他数据库连接来说是不可见的。
11 connection.rollback() 该方法回滚自上一次调用 commit() 以来对数据库所做的更改。
12 connection.close() 该方法关闭数据库连接。请注意,这不会自动调用 commit()。如果您之前未调用 commit() 方法,就直接关闭数据库连接,您所做的所有更改将全部丢失!
13 cursor.fetchone() 该方法获取查询结果集中的下一行,返回一个单一的序列,当没有更多可用的数据时,则返回 None。
14 cursor.fetchmany([size=cursor.arraysize]) 该方法获取查询结果集中的下一行组,返回一个列表。当没有更多的可用的行时,则返回一个空的列表。该方法尝试获取由 size 参数指定的尽可能多的行。
15 cursor.fetchall() 该例程获取查询结果集中所有(剩余)的行,返回一个列表。当没有可用的行时,则返回一个空的列表。

selenium & webdriver

Selenium with Python中文文档

“Failed to decode response from marionette” message in Python/Firefox headless scraping script

Running Selenium WebDriver tests using Firefox headless mode on Ubuntu

How to deal with certificates using Python Selenium

chrome需要使用chromewebdriver
firefox需要使用geckodriver

google一下第一个就是。在macOS上brew可以直接装geckodriver,然而在服务器上还是要wget

如果没有添加PATH,在初始化webdriver的时候加一下executable_path

1
webdriver.Firefox(executable_path='./path/to/geckodriver')

firefox headless

在服务器上跑肯定是没屏幕没GUI的,所以要多添加headless选项或者选择配置下虚拟屏幕,二选一:

  • 在代码中,给webdriver添加options。
1
2
3
firefox_options = webdriver.FirefoxOptions()
firefox_options.headless = True
driver = webdriver.Firefox(options=options)
  • 在执行脚本前使用Xvfb,以apt包管理为例:
1
2
3
4
5
6
sudo apt-get update
sudo apt-get install firefox xvfb
Xvfb :10 -ac &
export DISPLAY=:10

firefox

Message: Failed to decode response from marionette

如果在爬虫运行的开始就遇到这个异常,那么最大的可能是已经有firefox进程在占用端口,所以可以使用pkill firefox解决。

我的情况是,爬虫一开始运行地很好,第二天上午再去服务器上查看的时候发现崩溃了。完整异常信息是selenium.common.exceptions.WebDriverException: Message: Failed to decode response from marionette

并且,在geckodriver.log的最后中有如下记录:

1
2
A content process crashed and MOZ_CRASHREPORTER_SHUTDOWN is set, shutting down
1576182120579 Marionette INFO Stopped listening on port 33709

可以看到是firefox崩了,原因可能有很多。但是我猜最大的原因可能是内存泄漏,或者2GB RAM的服务器根本不够用。所以做出以下尝试,目前的效果还可以,没有再次遇到崩溃的情况。

禁用firefox缓存

如果是爬虫一直在用一个webdriver实例,也就说一直有个firefox进程在一直运行,内存占用确实可能会越来越大。初始化webdriver的时候禁用缓存可解决:

1
2
3
4
5
6
7
8
9
10
11
12
profile = webdriver.FirefoxProfile()
profile.set_preference("browser.cache.disk.enable", False)
profile.set_preference("browser.cache.memory.enable", False)
profile.set_preference("browser.cache.offline.enable", False)
profile.set_preference("network.http.use-cache", False)

firefox_options = webdriver.FirefoxOptions()
firefox_options.add_argument("--private")
firefox_options.headless = True

driver = webdriver.Firefox(firefox_profile=profile, options=firefox_options)

**Update:**感觉效果并不好,不知道是参数没有产生作用还是firefox有问题。只需要一个下午,firefox的内存占用就可以从15%到60%,问题可能就是在firefox这了,firefox内存泄漏??换用chrome试试,结果还是遇到了问题。selenium调用chrome访问目标网站返回空白页面,访问其它https页面则正常。然后添加了以下参数:

1
2
3
4
5
6
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_argument('--allow-running-insecure-content')
capabilities = DesiredCapabilities.CHROME.copy()
capabilities["acceptSslCerts"] = True
capabilities['acceptInsecureCerts'] = True

然而,在访问其它http站点时正常,在返回目标网页时依旧是空白页面(目标网站是jsp页面,不知道和这个有没有关系)。

实在无奈,完全不知道怎么解决,用回firefox。蠢办法,爬1000页之后重启webdriver和浏览器继续爬。

另外chrome的有些参数也是很有用的:

1
2
3
4
5
chrome_options.add_argument("--headless")
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument("--disable-application-cache")
chrome_options.add_argument('blink-settings=imagesEnabled=false')
chrome_options.add_argument("--incognito")

添加delay

1
time.sleep(2) 

因为stackoverflow上有人说可能是遇到了竞争条件,我也不太清楚为什么geckodriver和firefox之间的通信会出现竞争。由于目标网站的简单性,还有网站本身响应就很慢,我一开始写的时候确实1s的delay都没加,考虑到减轻我和目标服务器的负担,加上总归是好点。

而且如果你的爬虫启动了多个浏览器,delay可以让它们稍微歇一歇,免得跑一半又出问题。

后来,这个帖子update了一下,加了delay他的爬虫还是崩了,哈哈哈果然不对,然后他选择加内存。

增大server RAM

加钱,冲冲冲。 钱也解决不了内存泄漏