首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"resume-platform/pkg/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Parser interface {
|
||||
Parse(fileName string, reader io.Reader) (string, error)
|
||||
}
|
||||
|
||||
func NewParser(fileName string) (Parser, error) {
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
switch ext {
|
||||
case ".pdf":
|
||||
return &PDFParser{}, nil
|
||||
case ".docx":
|
||||
return &DOCXParser{}, nil
|
||||
case ".xlsx":
|
||||
return &ExcelParser{}, nil
|
||||
case ".md", ".markdown":
|
||||
return &MarkdownParser{}, nil
|
||||
case ".txt":
|
||||
return &TextParser{}, nil
|
||||
default:
|
||||
return &TextParser{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ParseFile(fileName string, reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parser, err := NewParser(fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
text, err := parser.Parse(fileName, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if text == "" && len(data) > 0 {
|
||||
text = extractTextFromBinary(data)
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func extractTextFromBinary(data []byte) string {
|
||||
content := string(data)
|
||||
var builder strings.Builder
|
||||
|
||||
for _, r := range content {
|
||||
if r >= 0x20 && r <= 0x7E || r == '\n' || r == '\r' || r == '\t' ||
|
||||
(r >= 0x4E00 && r <= 0x9FFF) || (r >= 0x3000 && r <= 0x303F) ||
|
||||
(r >= 0xFF00 && r <= 0xFFEF) {
|
||||
builder.WriteRune(r)
|
||||
} else if r > 0 {
|
||||
builder.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
|
||||
return cleanText(builder.String())
|
||||
}
|
||||
|
||||
type TextParser struct{}
|
||||
|
||||
func (p *TextParser) Parse(fileName string, reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cleanText(string(data)), nil
|
||||
}
|
||||
|
||||
type MarkdownParser struct{}
|
||||
|
||||
func (p *MarkdownParser) Parse(fileName string, reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cleanText(string(data)), nil
|
||||
}
|
||||
|
||||
type PDFParser struct{}
|
||||
|
||||
func (p *PDFParser) Parse(fileName string, reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
if !strings.HasPrefix(content, "%PDF-") {
|
||||
return "", fmt.Errorf("not a valid PDF file")
|
||||
}
|
||||
|
||||
text := extractTextFromPDF(content)
|
||||
return cleanText(text), nil
|
||||
}
|
||||
|
||||
func extractTextFromPDF(content string) string {
|
||||
var builder strings.Builder
|
||||
inTextBlock := false
|
||||
inStream := false
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if strings.Contains(line, "/Length") && !inStream {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "stream") {
|
||||
inStream = true
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "endstream") {
|
||||
inStream = false
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "BT") {
|
||||
inTextBlock = true
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, "ET") {
|
||||
inTextBlock = false
|
||||
continue
|
||||
}
|
||||
|
||||
if inTextBlock && !inStream {
|
||||
if strings.HasPrefix(line, "(") && strings.HasSuffix(line, ")") {
|
||||
text := line[1 : len(line)-1]
|
||||
text = strings.ReplaceAll(text, "\\n", "\n")
|
||||
text = strings.ReplaceAll(text, "\\r", "\r")
|
||||
text = strings.ReplaceAll(text, "\\t", "\t")
|
||||
text = strings.ReplaceAll(text, "\\(", "(")
|
||||
text = strings.ReplaceAll(text, "\\)", ")")
|
||||
text = strings.ReplaceAll(text, "\\/", "/")
|
||||
builder.WriteString(text)
|
||||
builder.WriteString(" ")
|
||||
} else if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
content := line[1 : len(line)-1]
|
||||
parts := strings.Fields(content)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "(") && strings.HasSuffix(part, ")") {
|
||||
text := part[1 : len(part)-1]
|
||||
text = strings.ReplaceAll(text, "\\n", "\n")
|
||||
text = strings.ReplaceAll(text, "\\r", "\r")
|
||||
text = strings.ReplaceAll(text, "\\t", "\t")
|
||||
builder.WriteString(text)
|
||||
builder.WriteString(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
|
||||
type DOCXParser struct{}
|
||||
|
||||
func (p *DOCXParser) Parse(fileName string, reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG DOCXParser: file size: %d bytes\n", len(data))
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open DOCX as zip: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG DOCXParser: zip contains %d files\n", len(zipReader.File))
|
||||
|
||||
var text strings.Builder
|
||||
foundDocument := false
|
||||
|
||||
for _, file := range zipReader.File {
|
||||
fmt.Printf("DEBUG DOCXParser: zip file: %s (size: %d)\n", file.Name, file.UncompressedSize64)
|
||||
|
||||
if file.Name == "word/document.xml" {
|
||||
foundDocument = true
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open document.xml: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
xmlData, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read document.xml: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG DOCXParser: document.xml size: %d bytes\n", len(xmlData))
|
||||
|
||||
parsedText := parseDOCXXML(string(xmlData))
|
||||
fmt.Printf("DEBUG DOCXParser: parsed text from document.xml: %d chars\n", len(parsedText))
|
||||
|
||||
text.WriteString(parsedText)
|
||||
} else if strings.HasPrefix(file.Name, "word/header") && strings.HasSuffix(file.Name, ".xml") {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
xmlData, _ := io.ReadAll(f)
|
||||
text.WriteString(parseDOCXXML(string(xmlData)))
|
||||
text.WriteString("\n")
|
||||
} else if strings.HasPrefix(file.Name, "word/footer") && strings.HasSuffix(file.Name, ".xml") {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
xmlData, _ := io.ReadAll(f)
|
||||
text.WriteString(parseDOCXXML(string(xmlData)))
|
||||
text.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if !foundDocument {
|
||||
fmt.Println("WARN DOCXParser: document.xml not found in zip")
|
||||
return cleanText(extractTextFromBinary(data)), nil
|
||||
}
|
||||
|
||||
result := cleanText(text.String())
|
||||
fmt.Printf("DEBUG DOCXParser: final result length: %d chars\n", len(result))
|
||||
|
||||
if len(result) > 0 {
|
||||
fmt.Printf("DEBUG DOCXParser: first 500 chars:\n%s\n", result[:utils.Min(len(result), 500)])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseDOCXXML(xmlContent string) string {
|
||||
fmt.Printf("DEBUG parseDOCXXML: input length: %d chars\n", len(xmlContent))
|
||||
|
||||
xmlContent = removeNamespacePrefixes(xmlContent)
|
||||
|
||||
allTexts := extractTextContent(xmlContent)
|
||||
|
||||
var builder strings.Builder
|
||||
for _, text := range allTexts {
|
||||
text = strings.TrimSpace(text)
|
||||
if text != "" {
|
||||
builder.WriteString(text)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
result := cleanText(builder.String())
|
||||
fmt.Printf("DEBUG parseDOCXXML: extracted %d text elements, result length: %d\n", len(allTexts), len(result))
|
||||
|
||||
if len(result) > 0 {
|
||||
fmt.Printf("DEBUG parseDOCXXML: first 500 chars:\n%s\n", result[:utils.Min(len(result), 500)])
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
fmt.Println("WARN: Structured parsing returned empty, using fallback")
|
||||
return cleanText(extractPlainTextFromXML(xmlContent))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func extractTextContent(xmlContent string) []string {
|
||||
var texts []string
|
||||
|
||||
inTag := false
|
||||
currentText := ""
|
||||
|
||||
for _, r := range xmlContent {
|
||||
if r == '<' {
|
||||
inTag = true
|
||||
if currentText != "" {
|
||||
texts = append(texts, currentText)
|
||||
currentText = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r == '>' {
|
||||
inTag = false
|
||||
continue
|
||||
}
|
||||
if !inTag {
|
||||
currentText += string(r)
|
||||
}
|
||||
}
|
||||
|
||||
if currentText != "" {
|
||||
texts = append(texts, currentText)
|
||||
}
|
||||
|
||||
return texts
|
||||
}
|
||||
|
||||
func removeNamespacePrefixes(xmlContent string) string {
|
||||
prefixes := []string{"w:", "r:", "wp:", "wpc:", "wps:", "wpg:", "wpi:", "wne:", "mc:", "o:", "v:", "w10:", "wpc:", "wpg:", "wps:", "a:", "m:"}
|
||||
for _, prefix := range prefixes {
|
||||
xmlContent = strings.ReplaceAll(xmlContent, "<"+prefix, "<")
|
||||
xmlContent = strings.ReplaceAll(xmlContent, "</"+prefix, "</")
|
||||
xmlContent = strings.ReplaceAll(xmlContent, " "+prefix, " ")
|
||||
}
|
||||
return xmlContent
|
||||
}
|
||||
|
||||
func extractPlainTextFromXML(xmlContent string) string {
|
||||
var builder strings.Builder
|
||||
inTag := false
|
||||
|
||||
for _, r := range xmlContent {
|
||||
if r == '<' {
|
||||
inTag = true
|
||||
continue
|
||||
}
|
||||
if r == '>' {
|
||||
inTag = false
|
||||
continue
|
||||
}
|
||||
if !inTag {
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type ExcelParser struct{}
|
||||
|
||||
func (p *ExcelParser) Parse(fileName string, reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open XLSX as zip: %w", err)
|
||||
}
|
||||
|
||||
var text strings.Builder
|
||||
for _, file := range zipReader.File {
|
||||
if strings.HasPrefix(file.Name, "xl/worksheets/") && strings.HasSuffix(file.Name, ".xml") {
|
||||
sheetName := strings.TrimPrefix(file.Name, "xl/worksheets/sheet")
|
||||
sheetName = strings.TrimSuffix(sheetName, ".xml")
|
||||
text.WriteString("===" + sheetName + "===\n\n")
|
||||
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open worksheet: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
xmlData, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read worksheet: %w", err)
|
||||
}
|
||||
|
||||
text.WriteString(parseXLSXXML(string(xmlData)))
|
||||
text.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return cleanText(text.String()), nil
|
||||
}
|
||||
|
||||
func parseXLSXXML(xmlContent string) string {
|
||||
type Cell struct {
|
||||
Text string `xml:",chardata"`
|
||||
}
|
||||
type Row struct {
|
||||
Cells []Cell `xml:"c"`
|
||||
}
|
||||
type SheetData struct {
|
||||
Rows []Row `xml:"row"`
|
||||
}
|
||||
type Worksheet struct {
|
||||
SheetData SheetData `xml:"sheetData"`
|
||||
}
|
||||
|
||||
var ws Worksheet
|
||||
err := xml.Unmarshal([]byte(xmlContent), &ws)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
for _, row := range ws.SheetData.Rows {
|
||||
var cells []string
|
||||
for _, cell := range row.Cells {
|
||||
text := strings.TrimSpace(cell.Text)
|
||||
if text != "" {
|
||||
cells = append(cells, text)
|
||||
}
|
||||
}
|
||||
if len(cells) > 0 {
|
||||
builder.WriteString(strings.Join(cells, "\t"))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(builder.String())
|
||||
}
|
||||
|
||||
func cleanText(text string) string {
|
||||
return utils.CleanText(text)
|
||||
}
|
||||
Reference in New Issue
Block a user