Golang如何将所需数据量写入CSV文件

Golang如何将所需数据量写入CSV文件 我正在尝试编写一个爬虫,从RFC网站的正文中获取数据,然后将数据解析到CSV文件中。同时,我还试图获取长度大于3的前20个最流行的单词。不幸的是,我似乎无法得到我想要的数量。这是我的结果:

Screenshot%20(4)

它基本上获取了所有单词及其出现次数(以映射的形式,即键和值),并且忽略了我指定的限制。

我认为问题出在第152行附近。这是完整代码的链接: https://play.golang.org/p/RZ9iCRRKypA

以下是执行解析的函数:

func writeSlice(results chan []MyMap, outfile string, headers []string) error {
	f, err := os.OpenFile(outfile, os.O_RDONLY|os.O_CREATE, 0666)
	if err != nil {
		return fmt.Errorf("couldnt open file %s", outfile)
	}
	defer f.Close()

	w := csv.NewWriter(f)

	w.Flush()

	if err = w.Write(headers); err != nil {
		return fmt.Errorf("couldnt write headers to file %s", outfile)
	}
	data := make([]string, 0, len(results))
	for v := range results {
		for i := 0; i < 20; i++ {
			data = append(data, v[i].key)
			data = append(data, strconv.Itoa(v[i].value))
			if err = w.Write(data); err != nil {
				return fmt.Errorf("couldnt write headers to file %s", outfile)
			}
		}
	}
	if err = w.Error(); err != nil {
		return fmt.Errorf("couldnt write headers to file %s", outfile)
	}
	return nil
}

我的问题在于,我不确定如何具体操作,只获取具有特定长度的前20个结果,并使用w.Write()写入结果。


更多关于Golang如何将所需数据量写入CSV文件的实战教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

非常感谢您付出的时间。我会花时间仔细研究您的实现,如果有需要,也会向您提问。再次感谢。

更多关于Golang如何将所需数据量写入CSV文件的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


如果你想只考虑包含字母的单词,请移除 || unicode.IsNumber(c)

是的,我只需要单词。

JustinObanor: 如果我只是想输出结果,我可以这样做。

是否可以在单线程下完成?暂时不使用并发?

是的,这是可能的。我们可以仅使用单线程来获取结果。但我怀疑在不并发运行的情况下能否获得多个结果,因为我正尝试遍历超过1000个RFC站点。

Christophe_Meessen:

然后将数据解析到csv文件

抱歉,我的意思是我正尝试将结果写入csv文件。结果是指长度大于3个字母的前20个单词。

如果我只是想输出结果,可以这样做:

for i := 0; i < *concurrency; i++ {
		go func() {
			for value := range tasks {
				site, err := scraper(value.url, value.id, query)
				if err != nil {
					fmt.Printf("error parsing data %v", err)
				}
				for i := 0; i < 20; i++ {
					fmt.Printf("%s\t : %d\n", site[i].key, site[i].value)
				}
				results <- site
			}
			wg.Done()
		}()
	}

还不知道如何检查单词长度是否大于3。

抱歉,之前使用我自己实现的堆的 mostFrequentWords 函数是错误的。

我已经实现并修正了这些函数。我还对它们进行了基准测试,以找出最高效的那个。 你可以在我的 github 仓库 中找到它们。

以下是最简单且最高效的实现:

func mostFrequentWords(m map[string]int, nbrWords int) []elem {
	h := elemHeap(make([]elem, nbrWords))
	last := nbrWords - 1
	for word, count := range m {
		if count < h[last].count || (count == h[last].count && word > h[last].word) {
			continue
		}
		pos := sort.Search(nbrWords, func(i int) bool { return h[i].count < count || (h[i].count == count && h[i].word > word) })
		copy(h[pos+1:], h[pos:last])
		h[pos] = elem{count: count, word: word}
	}
	if len(m) < nbrWords {
		return h[:len(m)]
	}
	return h
}

抱歉造成了困扰。

我对其进行了修改,以便可以传递一个字符串切片作为参数,因为我的大部分代码都在使用切片。希望这是一个好的方法。

package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	text := []string{"this is my is is my wow so is is my my so hey no yes"}
	m := countWordsIn(text)
	for k, v := range m {
		fmt.Printf("%d : %s\n", v, k)
	}
}

func countWordsIn(text []string) map[string]int {
	var wordBegPos, runeCount int
	wordCounts := make(map[string]int)
	texts := strings.Join(text, " ")
	for i, c := range texts {
		if unicode.IsLetter(c) {
			if runeCount == 0 {
				wordBegPos = i
			}
			runeCount++
			continue
		}
		if runeCount > 3 {
			word := texts[wordBegPos:i]
			count := wordCounts[word]
			count++
			wordCounts[word] = count
		}
		runeCount = 0
	}
	return wordCounts
}

我修改了代码,以便能够传递一个字符串切片作为参数,因为我的大部分代码都在使用切片。希望这是一个好的方法。

我有点困惑。你为什么需要一个字符串切片?每个RFC都可以加载到一个文本字符串中。

以下是爬虫函数的一个示例实现。如你所见,RFC文本被完整地读入 b,这是一个字节切片。

func scraper(url string, id int) (map[string]int, error) {
	client := &http.Client{
		Timeout: time.Minute,
	}
	resp, err := client.Get(url)
	if resp != nil {
		defer resp.Body.Close()
	}
	if err != nil {
		return nil, fmt.Errorf("error making request %v", err)
	}
	b, err := ioutil.ReadAll(resp.Body) 
	if err != nil {
		return nil, fmt.Errorf("error reading RFC text %v", err)
	}
	m := countWordsIn(string(b))
	return m, nil
}

然后,主程序将为每个URL调用爬虫函数,并将返回的结果累加以汇总所有已爬取RFC中的单词计数,如果这是你想要做的事情的话。

正如 @hollowaykeanho 所建议的,首先实现一个单线程版本,并确保其正常工作,然后再实现并发版本。

以下是 mostFrequentWords 的一个更优实现。它比堆的实现更简单,但效率稍低。其主要优点是,结果与你构建一个映射内容的切片、进行排序并返回前 nbrWords 个条目的子切片所得到的结果完全相同。该算法非常简单。它构建一个包含 nbrWords 个元素的列表,并保持其排序状态,根据需要插入新元素。

func mostFrequentWords(m map[string]int, nbrWords int) []elem {
	h := elemHeap(make([]elem, 0, nbrWords))
	last := nbrWords - 1
	for word, count := range m {
		if len(h) < nbrWords {
			pos := sort.Search(len(h), func(i int) bool { return h[i].count < count || (h[i].count == count && h[i].word >= word) })
			if pos == len(h) {
				h = append(h, elem{count: count, word: word})
				continue
			}
			h = append(h, elem{})
			copy(h[pos+1:], h[pos:len(h)])
			h[pos] = elem{count: count, word: word}
			continue
		}
		if count >= h[last].count {
			pos := sort.Search(nbrWords, func(i int) bool { return h[i].count < count || (h[i].count == count && h[i].word >= word) })
			copy(h[pos+1:], h[pos:last])
			h[pos] = elem{count: count, word: word}
		}
	}
	return h
}

这是不使用堆包(该包需要定义 elemHeap 类型和函数)的 mostFrequentWord 函数。它在内部实现了 heap.Fix() 函数。

type elem struct {
    word  string
    count int
}

func mostFrequentWords(m map[string]int, nbrWords int) []elem {
	h := elemHeap(make([]elem, nbrWords))
	for word, count := range m {
		if count < h[0].count {
			continue
		}
		h[0] = elem{word: word, count: count}
		for i, j := 0, 0; i < nbrWords; i = j {
			j := i * 2
			if j < nbrWords && h[i].count > h[j].count {
				h[j], h[i] = h[i], h[j]
				continue
			}
			j++
			if j < nbrWords && h[i].count > h[j].count {
				h[j], h[i] = h[i], h[j]
				continue
			}
			break
		}
	}
	sort.Slice(h, func(i, j int) bool { return h[i].count > h[j].count })
	return h
}

countWordsIn() 的实现存在一个小问题。单词将是 RFC 文本的子切片。其副作用是,RFC 文本将不会被垃圾回收,我们最终可能会在内存中保留所有 RFC 文本。我们不必为此担心,因为一份 RFC 文本并不那么大,而且 RFC 的数量也不多。这最多只需要几兆字节的内存。

为了避免这种情况,我们必须为用作 wordCounts 映射键的单词创建一个副本。遗憾的是,在 Go 中没有简单且可靠的方法来做到这一点。

问题出在writeSlice函数中,你从通道读取了所有结果,但只对每个结果的前20个元素进行了处理。实际上,你需要先收集所有数据,然后进行排序和筛选,最后只写入前20个符合条件的记录。

以下是修改后的writeSlice函数,它会先收集所有数据,然后筛选出长度大于3的单词,按出现频率排序,最后只写入前20个:

func writeSlice(results chan []MyMap, outfile string, headers []string) error {
    f, err := os.Create(outfile)
    if err != nil {
        return fmt.Errorf("couldnt open file %s: %v", outfile, err)
    }
    defer f.Close()

    w := csv.NewWriter(f)
    defer w.Flush()

    // 写入表头
    if err := w.Write(headers); err != nil {
        return fmt.Errorf("couldnt write headers to file %s: %v", outfile, err)
    }

    // 收集所有数据
    var allItems []MyMap
    for v := range results {
        allItems = append(allItems, v...)
    }

    // 筛选长度大于3的单词
    var filteredItems []MyMap
    for _, item := range allItems {
        if len(item.key) > 3 {
            filteredItems = append(filteredItems, item)
        }
    }

    // 按值(出现次数)降序排序
    sort.Slice(filteredItems, func(i, j int) bool {
        return filteredItems[i].value > filteredItems[j].value
    })

    // 只取前20个
    limit := 20
    if len(filteredItems) < limit {
        limit = len(filteredItems)
    }

    // 写入数据
    for i := 0; i < limit; i++ {
        record := []string{
            filteredItems[i].key,
            strconv.Itoa(filteredItems[i].value),
        }
        if err := w.Write(record); err != nil {
            return fmt.Errorf("couldnt write data to file %s: %v", outfile, err)
        }
    }

    return nil
}

另外,你需要确保MyMap类型是可导出的,并且添加排序功能:

type MyMap struct {
    Key   string
    Value int
}

// 在main函数或其他地方添加排序逻辑
func sortMyMaps(maps []MyMap) []MyMap {
    sort.Slice(maps, func(i, j int) bool {
        if maps[i].Value == maps[j].Value {
            return maps[i].Key < maps[j].Key
        }
        return maps[i].Value > maps[j].Value
    })
    return maps
}

这个修改后的版本会:

  1. 收集所有从通道传来的数据
  2. 筛选出键(单词)长度大于3的项
  3. 按值(出现次数)降序排序
  4. 只取前20个结果写入CSV文件

注意:我还修改了文件打开方式为os.Create,这会清空已存在的文件内容,确保每次写入都是新的数据。

回到顶部