首页 > 解决方案 > 根据他们的“汽车”拉出子列表

问题描述

我正在尝试使用 elisp 构建一个网络爬虫,因此我已将目标网站 ( https://weather.naver.com ) 的 HTML 解析为一个巨大的列表。例如,此列表是原始 HTML 的列表片段:

(defvar *test* '(li
                 ((class . "item_time")
                  (data-tmpr . "2")
                  (data-wetr-cd . "7")
                  (data-wetr-txt . "흐림")
                  (data-is-daytime . "false")
                  (data-ymdt . "2020121100"))
                 "
                                    "
                 (span
                  ((class . "time day"))
                  "0시")
                 (i
                  ((class . "ico _cnLazy  night")
                   (data-ico . "ico_wt7"))
                  (span
                   ((class . "blind"))
                   "흐림"))
                 "
                                    "
                 (span
                  ((class . "blind"))
                  "2도")
                 "
                                "))

car并想通过他们的 s拉出子列表。例如,我想通过给出和作为输入来提取所有以data-tmprfrom开头的子列表。到目前为止,我已经编写了这段代码:*test**test*'data-tmpr

(defun dotted-list-p (list)
  (and (listp list) (not (listp (cdr list)))))


(defun mapdot (func coll)
  `(,(funcall func (car coll)) . ,(funcall func (cdr coll))))


(defun mapfunc (tag coll)
  (if (listp coll)
      (if (equal (car coll) tag)
          coll
        (if (dotted-list-p coll)
            (if (equal (car coll) tag)
                coll
              '())
          '())
        (reduce #'append (mapcar (apply-partially #'mapfunc tag) coll) :initial-value '()))
    '()))


(defun find-by-tag (parsed-html tag)
  (if (equal (car parsed-html) tag)
      parsed-html
    (mapcar (apply-partially #'mapfunc tag) parsed-html)))

但是,计算以下表达式会遇到错误:

ELISP> (find-by-tag *test* 'class)
*** Eval error ***  Wrong type argument: listp, "2"

我对错误消息感到困惑,因为"2"必须来自点对'(data-tmpr . "2"),因此递归调用(find-by-tag '(data-tmpr . "2") 'class)应该nil因为这个条件表达式而产生:

(if (dotted-list-p coll)
    (if (equal (car coll) tag)
         coll
         '())
    '())

为什么评估会(find-by-tag *test* 'class)导致错误?

编辑:错字。Edit2:更好的标题。

标签: listcommon-lispelispsublist

解决方案


与 leetwinski 的回答一样,我不会回答您提出的直接问题,而是退后一步,尝试解决根本问题。

我还将在 CL 中进行此操作(common-lisp在标签中!):我确信这可以在 elisp 中完成,但是,好吧。

首先,您不是在这里处理一堆 conses:您正在处理一些树结构,这就是您应该处理的结构。该结构恰好被表示为一堆 conses,但唯一关心它的代码应该是进行抽象的代码。

这是此类代码的示例。我使用了错误的名称,因为这实际上是对 HTML(或 XML)的抽象,但我写得很快。

  • 一棵树要么是一个节点,要么是一个blob;
  • 节点具有名称、属性列表和子节点列表,它们是树;
  • blob 不是节点,也没有任何已定义的属性。
  • 属于节点的属性具有名称和值。

请注意,当我说“节点具有子列表”时,我的意思是:当您真正想要的是列表时,可以谈论列表。下面是实现这个抽象的代码:

;;; Things in the tree are either blobs or nodes
;;;

(defun treep (thing)
  (or (nodep thing) (blobp thing)))

(defun blobp (thing)
  (atom thing))

(defun nodep (thing)
  (and (consp thing)
       (listp (cdr thing))))

;;; Accessors for nodes
;;;              

(defun node-name (node)
  (assert (nodep node) (node)
    "~S isn't a node" node)
  (car node))

(defun node-attributes (node)
  (assert (nodep node) (node)
    "~S isn't a node" node)
  (cadr node))

(defun node-children (node)
  (assert (nodep node) (node)
    "~S isn't a node" node)
  (cddr node))

;;; predicate & accessors for attributes
;;;

(defun attributep (thing)
  (and (consp thing)
       (not (listp (cdr thing)))))

(defun attribute-name (attribute)
  (assert (attributep attribute) (attribute)
    "~S isnt an attribute" attribute)
  (car attribute))

(defun attribute-value (attribute)
  (assert (attributep attribute) (attribute)
    "~S isnt an attribute" attribute)
  (cdr attribute))

;;; Tree constructors (unused below)
;;;

(defun make-node (name &key
                       (attributes '())
                       (children '()))
  (assert (every #'attributep attributes) (attributes)
    "attributes ~S aren't" attributes)
  (assert (every #'treep children) (children)
    "children ~S aren't" children)
  (list* name attributes children))

(defun make-attribute (name value)
  (assert (not (listp value)) (value)
    "attribute values can't be lists because, sorry")
  (cons name value))

请注意,此处的检查数量可能过多。但是,好吧,如果产生这些树的东西把它们搞砸了,我想知道。

所以现在我们对我们正在制作的树有了一个抽象,我们现在可以编写代码来遍历这些树寻找东西,只使用这个抽象。这样做的一个好方法是编写一个walker,它是一个可以为我们遍历树的函数,在它找到的各种东西上调用访问者函数。这是一个简单的实现:请注意,访问者获得了他们上方的节点堆栈,他们可以使用这些节点来知道他们在哪里。

(defun walk-tree (tree &key
                       (node-visitor nil node-visitor-p)
                       (attribute-visitor nil attribute-visitor-p)
                       (blob-visitor nil blob-visitor-p))
  ;; A simple-minded recursive walker.  Visitors get two arguments:
  ;; the thing they are visiting and the stack (a list) of nodes above
  ;; it.
  (labels ((walk (thing stack)
             (cond 
              ((nodep thing)
               (let ((new-stack (cons thing stack)))
                 (when node-visitor-p
                   ;; node visitors want the current stack, not the
                   ;; new one
                   (funcall node-visitor thing stack))
                 (when attribute-visitor-p
                   ;; attribute visitors need the new stack so they
                   ;; can know which node they are attributes of
                   (dolist (a (node-attributes thing))
                     (funcall attribute-visitor a new-stack)))
                 (dolist (c (node-children thing))
                   ;; and the new stack is what we pass down
                   (walk c new-stack))))
              ((blobp thing)
               (when blob-visitor-p
                 ;; blob visitors just want the current stack
                 (funcall blob-visitor thing stack)))
              (t
               (error "mutant horror")))))
    (walk tree '()))
  tree)

我们可以检查一下:

(defun print-tree-stacks (tree)
  (walk-tree tree
             :node-visitor
             (lambda (n s)
               (print (mapcar #'node-name
                              (cons n s)))))
  (values))

现在:

 > (print-tree-stacks *test*)

(li) 
(span li) 
(i li) 
(span i li) 
(span li) 

好吧,闻起来不错。请注意,walker 纯粹是根据树抽象来编写的:如果您更改了表示,那么 walker 中的任何内容都不会改变。所以现在我们可以使用这个 walker 来提取我们想要的任何信息。假设我们要查找具有指定名称的属性。

(defun extract-attributes-named (tree name)
  (let ((attrs '()))
    (walk-tree tree
               :attribute-visitor
               (lambda (a s)
                 (declare (ignore s))
                 (when (eql (attribute-name a) name)
                   (push a attrs))))
    (nreverse attrs)))

我们可以测试一下:

>  (mapcar #'attribute-value (extract-attributes-named *test* 'class))
("item_time" "time day" "ico _cnLazy  night" "blind" "blind")

诀窍是定义描述你想要做什么的抽象,然后使用这些抽象,而不需要深入到实际的表示中。


推荐阅读