一老友希望从Twitter上抓取特定主体的所有推文,打算用python+selenium试一下。

安装Selenium

目前Selenium支持的Python版本有2.7,3.2,3.3,3.4。我使用的是2.7。

sudo pip install selenium

我在mac上安装遇到错误:

OSError: [Errno 1] Operation not permitted: '/System/Library/Frameworks/Python.framework/Versions/2.7/selenium'

这是因为OSX El Capitan使用的新的安全策略不允许用户修改系统文件。系统的Python也处于保护之下。

对于这个问题可以使用Virtualenv这个工具。Virtualenv可以建立一个独立的Python运行环境。具体可以参加这个教程

安装virtualenv并建立一个新Python环境:

1
2
3
4
5
sudo pip install virtualenv
virtualenv --no-site-packages test_env
source test_env/bin/activate # 进入环境
pip install selenium # 这个时候安装已经不需要sudo了
deactivate # 退出环境

virtualenv解决了Python不能为每个项目独立设置包管理的问题。但是相较于npm的方式,还是很麻烦的。

基础使用

1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Firefox()
driver.get("http://www.python.org")
assert "Python" in driver.title
elem = driver.find_element_by_name("q")
elem.send_keys("pycon")
elem.send_keys(Keys.RETURN)
assert "No results found." not in driver.page_source
driver.close()

这是官网上的demo。我们先把这个跑通了。我运行的时候提示错误:

1
2
raise WebDriverException("The browser appears to have exited "
selenium.common.exceptions.WebDriverException: Message: The browser appears to have exited before we could connect. If you specified a log_file in the FirefoxBinary constructor, check it for details.

再运行一次就不会报错。目前还不知道原因。

Python的官网使用了一些被墙的资源,所以我吧代码改成了中国版的,注意Python2.7中处理中文还是比较麻烦的:

1
2
3
4
5
6
7
8
# coding=utf-8
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Firefox()
driver.get("http://www.baidu.com")
assert u"百度" in driver.title
driver.close()

成功运行。

使用代理

目标链接:https://twitter.com/search?q=%23%24aapl%20lang%3Aen%20since%3A2015-01-01%20until%3A2015-12-31&src=typd。是Twitter的一个网址。还是必须得翻墙。

我使用的翻墙方法是在mac下使用chrome+shadowsocks。但是mac下的Firefox似乎无法使用系统代理,导致不能像chrome那样使用系统代理翻墙,需要手工指定。

而使用selenium启动的Firefox,并不会使用你正常启动的那个Firefox中的配置。需要在程序中指定(参考地址)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.proxy import *

myProxy = "127.0.0.1:1080"

proxy = Proxy({
'proxyType': ProxyType.MANUAL,
# 'httpProxy': myProxy,
# 'ftpProxy': myProxy,
# 'sslProxy': myProxy,
'socksProxy': myProxy,
'noProxy': '' # set this value as desired
})
driver = webdriver.Firefox(proxy=proxy)
driver.get("https://twitter.com/search?q=%23%24aapl%20lang%3Aen%20since%3A2015-01-01%20until%3A2015-12-31&src=typd")
# driver.close()

但是Firefox对代理的支持还是不尽如人意,通用使用代理,比chrome的速度慢了很多,而且还打不开。。。

使用ChromeDriver

ChromeDriver是用来控制Chrome的WebDriver。默认selenium不会提供ChromeDriver,需要自己去下载(地址)。

下载后把chromedriver这个可执行文件放到一个特定目录下。使用ChromeDriver需要制定这个路径的:

1
2
3
4
5
6
from selenium import webdriver
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome("/Users/mazhibin/software/chromedriver")
driver.get("https://twitter.com/search?q=%23%24aapl%20lang%3Aen%20since%3A2015-01-01%20until%3A2015-12-31&src=typd")
# driver.close()

很好,这些启动了Chrome,而且使用了系统代理,很快就打开了页面。(为什么Firefox一直打不开呢?)

获取页面内容

通过Chrome开发者工具分析Twitter页面。发现推文是在tweet-text这个类选择器中的。在selenium中如何获取呢?

首先是获取元素,selenium提供了一系列的函数来获取元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获取单个元素
find_element_by_id
find_element_by_name
find_element_by_xpath
find_element_by_link_text
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_class_name
find_element_by_css_selector

# 获取多个元素
find_elements_by_name
find_elements_by_xpath
find_elements_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elements_by_css_selector

这些函数返回WebElement对象。我们可以使用WebElement对象上属性和方法来获取信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
属性:
text
tag_name

方法:
get_attribute(name) # 获取元素的某属性
is_displayed() # 是否系列
is_enabled()
is_selected()
find_... # 之前的find系列在WebElement上也可以使用,表示寻找子元素
clear()
click()
screenshot(filename)
send_keys(*value)
submit()
value_of_css_property(property_name)

获取推文的text的demo如下:

1
2
3
4
5
6
driver = webdriver.Chrome(chromedriverPath)
driver.get(url)
items = driver.find_elements_by_css_selector(".tweet-text")
for item in items:
print item.text
driver.close()

滚动页面

Twitter的页面是会在滚动到页面底部后自动加载的。现在由于很多网站是这样的。就是因为页面中的信息是通过js异步加载的所以才需要使用selenium来抓取,不然用经典的Python爬虫就很好了。

现在我们需要通过selenium控制页面。

如果selenium对所有页面可能的操作都定义一个包装函数,可想而知,工作量是非常大的。而这些操作,可以肯定的是js一定能做到。所以selenium通过管道使用户可以在页面中注入js,使自身变得非常强大。

1
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

这句代码可以让页面滚动到底部。

现在的逻辑是,我需要在页面滚动到底部后,等待页面加载完成,再滚动到底部,再等待。重复十次,最后获取此时页面上的所有推文。

如果我直接重复上面的代码十次,是没有这个效果的。因为execute_script会立马返回,不会等待页面再次加载完成的。所以这个时候就需要wait系列函数了。

等待页面发生变化

大部分操作,是会导致页面变化的,页面变化后再进行下一步操作也是最常见的做法。为了让程序可以觉察到页面发生了变化,selenium提供了一系列等待页面发生变化的函数。

等待分为两种。一种是明确的等待,比如我知道点击这个按钮后,页面会多一个元素。另外一种是不明确等待,不明确等待因为不知道页面会发生什么变化,所以笼统地等待一定的时间。 前面的这种理解是错的。不明确等待,是指本来selenium在获取页面DOM元素时,是不会等待的,没有就是没有,而如果设置了不明确等待,那么如果没有获取到页面元素,会等待一定的时间。

明确等待的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Firefox()
driver.get("http://somedomain/url_that_delays_loading")
try:
element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.ID, "myDynamicElement"))
)
finally:
driver.quit()

expected_conditions中可以使用的条件有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
title_is
title_contains
presence_of_element_located
visibility_of_element_located
visibility_of
presence_of_all_elements_located
text_to_be_present_in_element
text_to_be_present_in_element_value
frame_to_be_available_and_switch_to_it
invisibility_of_element_located
element_to_be_clickable - it is Displayed and Enabled.
staleness_of
element_to_be_selected
element_located_to_be_selected
element_selection_state_to_be
element_located_selection_state_to_be
alert_is_present

不明确等待可以这么用:

1
2
3
4
5
6
from selenium import webdriver

driver = webdriver.Firefox()
driver.implicitly_wait(10) # seconds
driver.get("http://somedomain/url_that_delays_loading")
myDynamicElement = driver.find_element_by_id("myDynamicElement")

selenium似乎没有直接让浏览器等待几秒的做法。只能使用明确等待的系列函数。

现在回到Twitter的页面逻辑上。页面在滚动到底部后,页面上啥元素也不会边,底下也不会变成“正在加载”什么的(Twitter真是偷懒啊。。。)只能通过页面中推文的个数来确定了。

第一次页面会加载13个推文,之后每次都会加载13个,所以通过个数可以判断加载完成了。可以使用css的选择器,来实现这个效果。推文都是在#stream-items-id这个ol中的,其中的第一个li是标题,所以第2-14个是推文,如果第27个推文出现了,说明第一次的刷新成功了。

“#stream-items-id >li:nth-of-type(28)”

所以在页面滚动后,需要等待最新的一条推文出现,再继续滚动。最终的程序如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# coding=utf-8
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import csv
import codecs
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

def crawl(url):
chromedriverPath = "/Users/mazhibin/software/chromedriver"

# twitter每次更新13条
eachCount = 13

# 滚动几次。滚动次数越多,采集的数据越多
scrollTimes = 10

# driver = webdriver.Firefox()
driver = webdriver.Chrome(chromedriverPath)
driver.get(url)

try:
for i in range(scrollTimes):
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

lastTweetCss = "#stream-items-id >li:nth-of-type(" + str(eachCount*(i+2)+1) + ") .tweet-text"
print lastTweetCss
elem = WebDriverWait(driver,20).until(EC.visibility_of_element_located((By.CSS_SELECTOR,lastTweetCss)))
printTweet(driver)
finally:
driver.close()


def printTweet(driver):
tweetCss = "[data-item-type=tweet]"
nameCss = ".fullname"
timeCss = "._timestamp"
contentCss = ".tweet-text"

items = driver.find_elements_by_css_selector(tweetCss)
resultArr = []
for i,item in enumerate(items):
try:
nameElem = item.find_element_by_css_selector(nameCss)
timeElem = item.find_element_by_css_selector(timeCss)
contentElem = item.find_element_by_css_selector(contentCss)

name = nameElem.text
time = timeElem.text
content = contentElem.text

resultArr.append([name,time,content])
print("%d: name=%s time=%s content=%s" % (i,name,time,content))
except Exception, e:
print("error index=%d" % (i))
print e

printToCsv(resultArr)


def printToCsv(data):
writer = csv.writer(codecs.open('result.csv','wb','gbk'))
writer.writerow(['name','time','content'])

for item in data:
writer.writerow(item)


if __name__ == '__main__':
url = "https://twitter.com/search?q=%23%24aapl%20lang%3Aen%20since%3A2015-01-01%20until%3A2015-12-31&src=typd"
crawl(url)

参考链接