0%

如何区分一个页面是列表页还是详情页

解析页面是做爬虫的过程中的重要环节,而且如果站点多了,解析也会变得非常复杂,所以智能化解析就可能是一个不错的解决方案。如果我们能够容忍一定的错误率,那么我们可以利用智能化解析算法帮我们提取一些内容,简单高效。 那有没有办法做到一个网站的全自动化解析呢? 比如来了一个博客网站,我能首先识别出来这是一个列表页还是文章(详情)页,然后提取列表页的每篇文章的链接,然后跳转到每篇文章(详情)页再提取文章相关信息。 那么这里面可能就有四个关键部分:

  • 判断当前所在的页面是列表页还是文章(详情)页
  • 识别出列表页下一页的链接
  • 识别出列表页所有列表链接
  • 识别出文章(详情)页的文章内容和其他信息

如果我们能把这四步都用算法实现出来,那么我们只需要一个网站的主站链接就能轻松地把内容规整地爬取下来了。 那么这篇文章我们就来简单说下第一步,如何判断当前所在的页面的列表页还是文章(详情)页。

注:后文中文章页统一称之为详情页。

示例

列表页和详情页不知道大家有没有基本的概念了,列表页就是导航页,里面带有好多文章或新闻或详情链接,我们选一个链接点进去就是详情页。 比如说这里拿新浪体育来说,首页如图所示: image-20200720193407237 看到这里面有很多链接,就是一些页面导航集合,这个页面就是列表页。 然后我们随便点开一篇新闻报道,如图所示: image-20200720193504042 这里就是一篇新闻报道,带有醒目的标题、发布时间、正文等内容,这就是详情页。 现在我们要做的就是用一个算法来凭借 HTML 代码区分出来哪个是列表页,哪个是详情页。 最后的输入输出如下:

  • 输入:一个页面的 HTML 代码
  • 输出:这个页面是列表页还是详情页,并输出二者的判定概率。

模型选用

首先我们确认下这个问题是个什么问题。 很明显,结果要么是列表页,要么是详情页,就是个二分类问题。 那二分类问题怎么解决呢?实现一个基本的分类模型就好了。大范围就是传统机器学习和现在比较流行的深度学习。总体上来说,深度学习的精度和处理能力会强一点,但是想想我们现在的应用场景,后者要追求精度的话可能需要更多的标注数据,而前者也有比较不错的易用的模型了,比如 SVM。 所以,我们不妨先选用 SVM 模型来实现一个基本的二分类模型来试试看,效果如果已经很好了或者提升空间不大了,那就直接用就好了,如果效果比较差,那我们再选用其他模型来优化。 好,那就定下来了,我们用 SVM 模型来实现一下试试。

数据标注

既然要做分类模型,那么最重要的当然就是数据标注了,我们分两组就好了,一组是列表页,一组是详情页,我们先用手工配合爬虫找一些列表页和详情页的 HTML 代码,然后将其保存下来。 结果类似如下: image-20200720195132784 每个文件夹几百个就行了,数量不用太多,五花八门的页面混起来更好。

特征提取

既然要做 SVM,那么我们得想清楚要分清两个类别需要哪些特征。既然是特征,那我们就要选出二者不同的特征,这样更加有区分度。 比如这里我大体总结了有这么几个特征:

  • 文本密度:正文页通常会包含密集的文字,比如一个 p 节点内部就包含几十上百个文字,如果用单个节点内的文字数目来表示文本密度的话,那么详情页的部分内容文本密度会很高。
  • 超链接节点的数量和比例:一般来说列表页通常会包含多个超链接,而且很大比例都是超链接文本,而详情页却有很多的文字并不是超链接,比如正文。
  • 符号密度:一般来说列表页通常会是一些标题导航,一般可能都不会包含句号,而详情页的正文内容通常就会包含句号等内容,如果按照单位文字所包含的标点符号数量来表示符号密度的话,后者的符号密度也会高一些。
  • 列表簇的数目:一般来说,列表页通常会包含多个具有公共父节点的条目,多个条目构成一个列表簇,虽然说详情页侧栏也会包含一些列表,但至少这个数量也可以成为一个特征来判别。
  • meta 信息:有一些特殊的 meta 信息是列表页独有的,比如只有详情页才会有发布时间,而列表页通常是没有的。
  • 正文标题和 title 标题相似度:一般来说,详情页的正文标题和 title 标题很可能是相同的内容,而列表页通常则是站点的名称。

以上便是我简单列的几个特征,还有很多其他的特征也可以来挖掘,比如视觉特征等等。

模型训练

真正代码实现的过程就是将现有的 HTML 文本进行预处理,把上面的一些特征提取出来,然后直接声明一个 SVM 分类模型即可。 这里声明了一个 feature 名字和对应的处理方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.feature_funcs = {
'number_of_a_char': number_of_a_char,
'number_of_a_char_log10': self._number_of_a_char_log10,
'number_of_char': number_of_char,
'number_of_char_log10': self._number_of_char_log10,
'rate_of_a_char': self._rate_of_a_char,
'number_of_p_descendants': number_of_p_descendants,
'number_of_a_descendants': number_of_a_descendants,
'number_of_punctuation': number_of_punctuation,
'density_of_punctuation': density_of_punctuation,
'number_of_clusters': self._number_of_clusters,
'density_of_text': density_of_text,
'max_density_of_text': self._max_density_of_text,
'max_number_of_p_children': self._max_number_of_p_children,
'has_datetime_meta': self._has_datetime_mata,
'similarity_of_title': self._similarity_of_title,
}
self.feature_names = self.feature_funcs.keys()

以上方法就是特征和对应的获取方法,具体根据实际情况实现即可。 然后关键的部分就是对数据的处理和模型的训练了,关键代码如下:

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
list_file_paths = list(glob(f'{DATASETS_LIST_DIR}/*.html'))
detail_file_paths = list(glob(f'{DATASETS_DETAIL_DIR}/*.html'))

x_data, y_data = [], []

for index, list_file_path in enumerate(list_file_paths):
logger.log('inspect', f'list_file_path {list_file_path}')
element = file2element(list_file_path)
if element is None:
continue
preprocess4list_classifier(element)
x = self.features_to_list(self.features(element))
x_data.append(x)
y_data.append(1)

for index, detail_file_path in enumerate(detail_file_paths):
logger.log('inspect', f'detail_file_path {detail_file_path}')
element = file2element(detail_file_path)
if element is None:
continue
preprocess4list_classifier(element)
x = self.features_to_list(self.features(element))
x_data.append(x)
y_data.append(0)

# preprocess data
ss = StandardScaler()
x_data = ss.fit_transform(x_data)
joblib.dump(ss, self.scaler_path)
x_train, x_test, y_train, y_test = train_test_split(x_data, y_data, test_size=0.2, random_state=5)

# set up grid search
c_range = np.logspace(-5, 20, 5, base=2)
gamma_range = np.logspace(-9, 10, 5, base=2)
param_grid = [
{'kernel': ['rbf'], 'C': c_range, 'gamma': gamma_range},
{'kernel': ['linear'], 'C': c_range},
]
grid = GridSearchCV(SVC(probability=True), param_grid, cv=5, verbose=10, n_jobs=-1)
clf = grid.fit(x_train, y_train)
y_true, y_pred = y_test, clf.predict(x_test)
logger.log('inspect', f'n{classification_report(y_true, y_pred)}')
score = grid.score(x_test, y_test)
logger.log('inspect', f'test accuracy {score}')
# save model
joblib.dump(grid.best_estimator_, self.model_path)

这里首先对数据进行预处理,然后将每个 feature 存 存到 x_data 中,标注结果存到 y_data 中。接着我们使用 StandardScaler 对数据进行标准化处理,然后进行随机切分。最后使用 GridSearch 训练了一个 SVM 模型然后保存了下来。 以上便是基本的模型训练过程,具体的代码可以再完善一下。

使用

以上的流程我已经实现了,并且发布了一个开源 Python 包,名字叫做 Gerapy AutoExtractor,GitHub 地址为 https://github.com/Gerapy/GerapyAutoExtractor。 大家如需使用可以使用 pip 安装:

1
pip3 install gerapy-auto-extractor

这个库针对于以上算法提供了四个方法:

  • is_detail:判断是否是详情页
  • is_list:判断是否是列表页
  • probability_of_detail:是详情页的概率,结果是 0-1
  • probability_of_list:是列表页的概率,结果是 0-1

例如,我们随便可以找几个网址存下来,比如把上文的列表页和详情页的 HTML 代码存下来分别保存为 list.html 和 detail.html。 测试代码如下:

1
2
3
4
5
6
7
8
9
10
from gerapy_auto_extractor import is_detail, is_list, probability_of_detail, probability_of_list
from gerapy_auto_extractor.helpers import content, jsonify

html = content('detail.html')
print(probability_of_detail(html), probability_of_list(html))
print(is_detail(html), is_list(html))

html = content('list.html')
print(probability_of_detail(html), probability_of_list(html))
print(is_detail(html), is_list(html))

这里我们就调用了以上四个方法来实现了页面类型和置信度的判断。 类似的输出结果如下:

1
2
3
4
0.9990605314033392 0.0009394685966607814
True False
0.033477426883441685 0.9665225731165583
False True

这样我们就成功得到了两个页面的类别和置信度了。 以上便是判断列表页和详情页的原理和实现,如需了解更多请关注项目 Gerapy Auto Extractor,GitHub 链接为 https://github.com/Gerapy/GerapyAutoExtractor,多谢支持