Xudong's Blog

写定向爬虫时遇到的问题

Word count: 3.3kReading time: 13 min
2019/12/15

This is a [ Personal Note ]

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

rsync

rsync命令

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

Demo

1
$ rsync [email protected]:~/data.db ~/Desktop/
1
$ rsync -P [email protected]:~/big_db.db ~/Desktop/

-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
# C-a C-d
$ screen -ls
# There is a screen on:
# 25530.py_script (Detached)
# 1 Socket in /var/folders/by/r58v7b893473twk47z7rkqw80000gn/T/.screen.
$ 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
# -*- coding: utf-8 -*-
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()  # 不加名称设置root logger
logger.setLevel(logging.DEBUG) # 设置logger整体记录的level
formatter = logging.Formatter(
'%(asctime)s %(name)s:%(levelname)s:%(message)s',
datefmt='%Y-%m-%d %H:%M:%S')

# 使用FileHandler输出到文件
fh = logging.FileHandler(str(datetime.now().strftime('%Y-%m-%d_%H:%M:%S')) + '_ftban.log',
mode='a',
encoding='utf-8')
fh.setLevel(logging.INFO) # 输出到handler的level
fh.setFormatter(formatter)

# 使用StreamHandler输出到标准输出
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)

# 添加两个Handler
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") # try to disable browser cache
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

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

CATALOG
  1. 1. rsync
    1. 1.1. Demo
    2. 1.2. Manual
      1. 1.2.1. 语法
      2. 1.2.2. 选项
  2. 2. screen
    1. 2.1. Demo
    2. 2.2. Manual
      1. 2.2.1. 语法
      2. 2.2.2. 选项
      3. 2.2.3. 常用screen参数
  3. 3. Python3 utf-8
  4. 4. argparse
  5. 5. logging
    1. 5.1. Demo
    2. 5.2. logging同时输出到屏幕和文件
  6. 6. sqlite
    1. 6.1. Demo
    2. 6.2. APIs
  7. 7. selenium & webdriver
    1. 7.1. firefox headless
  8. 8. Message: Failed to decode response from marionette
    1. 8.1. 禁用firefox缓存
    2. 8.2. 添加delay
    3. 8.3. 增大server RAM